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 20d03e54..1a588f79 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, MoreVertical, UserRoundPen } from 'lucide-react'
+import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, FileText, Folder, List, Menu, MoreVertical, UserRoundPen, Video, Layers, ListFilter, ListTree } from 'lucide-react'
import { markActivityAsComplete } from '@services/courses/activity'
import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
@@ -121,13 +121,20 @@ function ActivityClient(props: ActivityClientProps) {
/>
-
-
- Chapter : {getChapterNameByActivityId(course, activity.id)}
-
-
- {activity.name}
-
+
+
+
+
+ Chapter : {getChapterNameByActivityId(course, activity.id)}
+
+
+ {activity.name}
+
+
{activity && activity.published == true && activity.content.paid_access != false && (
@@ -213,6 +220,16 @@ function ActivityClient(props: ActivityClientProps) {
)}
>
)}
+
+ {/* Activity Navigation */}
+ {activity && activity.published == true && activity.content.paid_access != false && (
+
+ )}
+
{
}
@@ -235,7 +252,7 @@ export function MarkStatus(props: {
const trail = await markActivityAsComplete(
props.orgslug,
props.course.course_uuid,
- 'activity_' + props.activityid,
+ props.activity.activity_uuid,
session.data?.tokens?.access_token
)
router.refresh()
@@ -393,4 +410,284 @@ function AssignmentTools(props: {
return null
}
+function ActivityChapterDropdown(props: {
+ course: any
+ currentActivityId: string
+ orgslug: string
+}): React.ReactNode {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const dropdownRef = React.useRef
(null);
+ const isMobile = useMediaQuery('(max-width: 768px)');
+
+ // Close dropdown when clicking outside
+ React.useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const toggleDropdown = () => {
+ setIsOpen(!isOpen);
+ };
+
+ // Function to get the appropriate icon for activity type
+ const getActivityTypeIcon = (activityType: string) => {
+ switch (activityType) {
+ case 'TYPE_VIDEO':
+ return ;
+ case 'TYPE_DOCUMENT':
+ return ;
+ case 'TYPE_DYNAMIC':
+ return ;
+ case 'TYPE_ASSIGNMENT':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ // Function to get the appropriate badge color for activity type
+ const getActivityTypeBadgeColor = (activityType: string) => {
+ return 'bg-gray-100 text-gray-600';
+ };
+
+ return (
+
+
+
+ {isOpen && (
+
+
+
Course Content
+
+
+
+
+ {props.course.chapters.map((chapter: any) => (
+
+
+
+ {chapter.activities.map((activity: any) => {
+ // Remove any prefixes from UUIDs
+ const cleanActivityUuid = activity.activity_uuid?.replace('activity_', '');
+ const cleanCourseUuid = props.course.course_uuid?.replace('course_', '');
+
+ return (
+
setIsOpen(false)}
+ >
+
+
+
+ {getActivityTypeIcon(activity.activity_type)}
+
+
+ {activity.name}
+
+
+ {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
+ ) && (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+function ActivityNavigation(props: {
+ course: any
+ currentActivityId: string
+ orgslug: string
+}): React.ReactNode {
+ const router = useRouter();
+ const isMobile = useMediaQuery('(max-width: 768px)');
+ const [isBottomNavVisible, setIsBottomNavVisible] = React.useState(true);
+ const bottomNavRef = React.useRef(null);
+ const [navWidth, setNavWidth] = React.useState(null);
+
+ // 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
+ props.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 === props.currentActivityId.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 = props.course.course_uuid?.replace('course_', '');
+ router.push(getUriWithOrg(props.orgslug, '') + `/course/${cleanCourseUuid}/activity/${activity.cleanUuid}`);
+ };
+
+ // Set up intersection observer to detect when bottom nav is out of viewport
+ // and measure the width of the bottom navigation
+ React.useEffect(() => {
+ if (!bottomNavRef.current) return;
+
+ // Update width when component mounts and on window resize
+ const updateWidth = () => {
+ if (bottomNavRef.current) {
+ setNavWidth(bottomNavRef.current.offsetWidth);
+ }
+ };
+
+ // Initial width measurement
+ updateWidth();
+
+ // Set up resize listener
+ window.addEventListener('resize', updateWidth);
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setIsBottomNavVisible(entry.isIntersecting);
+ },
+ { threshold: 0.1 }
+ );
+
+ observer.observe(bottomNavRef.current);
+
+ return () => {
+ window.removeEventListener('resize', updateWidth);
+ if (bottomNavRef.current) {
+ observer.unobserve(bottomNavRef.current);
+ }
+ };
+ }, []);
+
+ // Navigation buttons component - reused for both top and bottom
+ const NavigationButtons = ({ isFloating = false }) => (
+
+
+
+
+ {currentIndex + 1} of {allActivities.length}
+
+
+
+
+ );
+
+ return (
+ <>
+ {/* Bottom navigation (in-place) */}
+
+
+
+
+ {/* Floating bottom navigation - shown when bottom nav is not visible */}
+ {!isBottomNavVisible && (
+
+ )}
+ >
+ );
+}
+
export default ActivityClient