From e39c9c37badb8455d96e26f80a4c8dc9e05775c1 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 16 Jul 2025 20:14:30 +0200 Subject: [PATCH] feat: enhance course activity page with dynamic title and certification badge component --- .../activity/[activityid]/activity.tsx | 2 + .../activity/[activityid]/page.tsx | 8 +- .../Pages/Activity/CourseEndView.tsx | 265 ++++++++++++++---- .../Pages/Courses/ActivityIndicators.tsx | 68 ++++- 4 files changed, 288 insertions(+), 55 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 cb265b42..f868ef91 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 @@ -521,6 +521,8 @@ function ActivityClient(props: ActivityClientProps) { orgslug={orgslug} courseUuid={course.course_uuid} thumbnailImage={course.thumbnail_image} + course={course} + trailData={trailData} /> ) : (
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 f49295a6..40ef79e4 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 @@ -43,9 +43,13 @@ export async function generateMetadata(props: MetadataProps): Promise access_token || null ) + // Check if this is the course end page + const isCourseEnd = params.activityid === 'end'; + const pageTitle = isCourseEnd ? `Congratulations — ${course_meta.name} Course` : activity.name + ` — ${course_meta.name} Course`; + // SEO return { - title: activity.name + ` — ${course_meta.name} Course`, + title: pageTitle, description: course_meta.description, keywords: course_meta.learnings, robots: { @@ -59,7 +63,7 @@ export async function generateMetadata(props: MetadataProps): Promise }, }, openGraph: { - title: activity.name + ` — ${course_meta.name} Course`, + title: pageTitle, description: course_meta.description, publishedTime: course_meta.creation_date, tags: course_meta.learnings, diff --git a/apps/web/components/Pages/Activity/CourseEndView.tsx b/apps/web/components/Pages/Activity/CourseEndView.tsx index 027f1a65..a493ca78 100644 --- a/apps/web/components/Pages/Activity/CourseEndView.tsx +++ b/apps/web/components/Pages/Activity/CourseEndView.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import ReactConfetti from 'react-confetti'; -import { Trophy, ArrowLeft } from 'lucide-react'; +import { Trophy, ArrowLeft, BookOpen, Target } from 'lucide-react'; import Link from 'next/link'; import { getUriWithOrg } from '@services/config/config'; import { getCourseThumbnailMediaDirectory } from '@services/media/media'; @@ -12,68 +12,229 @@ interface CourseEndViewProps { orgslug: string; courseUuid: string; thumbnailImage: string; + course: any; + trailData: any; } -const CourseEndView: React.FC = ({ courseName, orgslug, courseUuid, thumbnailImage }) => { +const CourseEndView: React.FC = ({ + courseName, + orgslug, + courseUuid, + thumbnailImage, + course, + trailData +}) => { const { width, height } = useWindowSize(); const org = useOrg() as any; - return ( -
-
- -
+ // Check if course is actually completed + const isCourseCompleted = useMemo(() => { + if (!trailData || !course) return false; + + // Flatten all activities + const allActivities = course.chapters.flatMap((chapter: any) => + chapter.activities.map((activity: any) => ({ + ...activity, + chapterId: chapter.id + })) + ); + + // Check if all activities are completed + const isActivityDone = (activity: any) => { + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + const run = trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ); -
-
- {thumbnailImage && ( - {courseName} - )} + if (run) { + return run.steps.find( + (step: any) => step.activity_id === activity.id && step.complete === true + ); + } + return false; + }; + + const totalActivities = allActivities.length; + const completedActivities = allActivities.filter((activity: any) => isActivityDone(activity)).length; + return totalActivities > 0 && completedActivities === totalActivities; + }, [trailData, course]); + + // Calculate progress for incomplete courses + const progressInfo = useMemo(() => { + if (!trailData || !course || isCourseCompleted) return null; + + const allActivities = course.chapters.flatMap((chapter: any) => + chapter.activities.map((activity: any) => ({ + ...activity, + chapterId: chapter.id + })) + ); + + const isActivityDone = (activity: any) => { + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + const run = 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 && step.complete === true + ); + } + return false; + }; + + const totalActivities = allActivities.length; + const completedActivities = allActivities.filter((activity: any) => isActivityDone(activity)).length; + const progressPercentage = Math.round((completedActivities / totalActivities) * 100); + + return { + completed: completedActivities, + total: totalActivities, + percentage: progressPercentage + }; + }, [trailData, course, isCourseCompleted]); + + if (isCourseCompleted) { + // Show congratulations for completed course + return ( +
+
+ +
+ +
+
+ {thumbnailImage && ( + {courseName} + )} + +
+ +
+
-
- +

+ Congratulations! 🎉 +

+ +

+ You've successfully completed + {courseName} +

+ +

+ Your dedication and hard work have paid off. You've mastered all the content in this course. +

+ +
+ + + Back to Course +
- -

- Congratulations! 🎉 -

- -

- You've successfully completed - {courseName} -

- -

- Your dedication and hard work have paid off. You've mastered all the content in this course. -

+
+ ); + } else { + // Show progress and encouragement for incomplete course + return ( +
+
+
+ {thumbnailImage && ( + {courseName} + )} + +
+ +
+
+ +

+ Keep Going! 💪 +

+ +

+ You're making great progress in + {courseName} +

+ + {progressInfo && ( +
+
+ + Course Progress +
+ +
+
+ Progress + {progressInfo.percentage}% +
+ +
+
+
+ +
+ {progressInfo.completed} of {progressInfo.total} activities completed +
+
+
+ )} + +

+ You're doing great! Complete the remaining activities to unlock your course completion certificate. +

-
- - - Back to Course - +
+ + + Continue Learning + +
-
- ); + ); + } }; export default CourseEndView; \ No newline at end of file diff --git a/apps/web/components/Pages/Courses/ActivityIndicators.tsx b/apps/web/components/Pages/Courses/ActivityIndicators.tsx index 4eef89c9..882039bd 100644 --- a/apps/web/components/Pages/Courses/ActivityIndicators.tsx +++ b/apps/web/components/Pages/Courses/ActivityIndicators.tsx @@ -1,5 +1,5 @@ 'use client' -import { BookOpenCheck, Check, FileText, Layers, Video, ChevronLeft, ChevronRight } from 'lucide-react' +import { BookOpenCheck, Check, FileText, Layers, Video, ChevronLeft, ChevronRight, Trophy } from 'lucide-react' import React, { useMemo, memo, useState } from 'react' import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip' import { getUriWithOrg } from '@services/config/config' @@ -124,6 +124,58 @@ const ChapterTooltipContent = memo(({ ChapterTooltipContent.displayName = 'ChapterTooltipContent'; +// Add certification badge component +const CertificationBadge = memo(({ + courseid, + orgslug, + isCompleted +}: { + courseid: string, + orgslug: string, + isCompleted: boolean +}) => ( + +
+ + + {isCompleted ? 'Course Completed!' : 'Course Completion'} + +
+
+ + {isCompleted + ? 'View your completion certificate' + : 'Complete all activities to unlock your certificate' + } + +
+
+ } + > + +
+ +
+ + +)); + +CertificationBadge.displayName = 'CertificationBadge'; + function ActivityIndicators(props: Props) { const course = props.course const orgslug = props.orgslug @@ -218,6 +270,13 @@ function ActivityIndicators(props: Props) { }, 0) }, [isActivityDone]); + // Check if all activities are completed + const isCourseCompleted = useMemo(() => { + const totalActivities = allActivities.length; + const completedActivities = allActivities.filter((activity: any) => isActivityDone(activity)).length; + return totalActivities > 0 && completedActivities === totalActivities; + }, [allActivities, isActivityDone]); + return (
{enableNavigation && ( @@ -317,6 +376,13 @@ function ActivityIndicators(props: Props) { ) })} + + {/* Certification Badge */} +
{enableNavigation && (