diff --git a/src/components/FolderView.tsx b/src/components/FolderView.tsx index e5f5fae6f..e3aae97c1 100644 --- a/src/components/FolderView.tsx +++ b/src/components/FolderView.tsx @@ -1,6 +1,9 @@ 'use client'; import { useRouter } from 'next/navigation'; import { ContentCard } from './ContentCard'; +import { useRecoilValue } from 'recoil'; +import { videoCompletionAtom } from '@/store/atoms/videoCompletion'; +import { getFolderContentCompleted } from '@/lib/utils'; export const FolderView = ({ courseContent, @@ -32,24 +35,33 @@ export const FolderView = ({ updatedRoute += `/${rest[i]}`; } // why? because we have to reset the segments or they will be visible always after a video - + const videoCompletion = useRecoilValue(videoCompletionAtom); return (
- {courseContent.map((content) => ( - { - router.push(`${updatedRoute}/${content.id}`); - }} - markAsCompleted={content.markAsCompleted} - percentComplete={content.percentComplete} - /> - ))} + {courseContent.map((content) => { + let percent = null; + let isCompleted = false; + if (content.type === 'folder') { + percent = getFolderContentCompleted(videoCompletion , content.id); + } else if (content.type === 'video') { + isCompleted = videoCompletion.find((item) => item.id === content.id)?.isCompleted || false; + } + return ( + { + router.push(`${updatedRoute}/${content.id}`); + }} + markAsCompleted={isCompleted} + percentComplete={percent} + /> + ); + })}
); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index c95ec05b0..64ce4877e 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -11,8 +11,9 @@ import { Button } from './ui/button'; import { BackArrow } from '@/icons/BackArrow'; import { useRecoilState } from 'recoil'; import { sidebarOpen as sidebarOpenAtom } from '@/store/atoms/sidebar'; -import { useEffect, useState } from 'react'; -import { handleMarkAsCompleted } from '@/lib/utils'; +import { useEffect } from 'react'; +import { VideoCompletionData, convertToVideoCompletionData, handleMarkAsCompleted } from '@/lib/utils'; +import { videoCompletionAtom } from '@/store/atoms/videoCompletion'; export function Sidebar({ courseId, @@ -23,6 +24,11 @@ export function Sidebar({ }) { const router = useRouter(); const [sidebarOpen, setSidebarOpen] = useRecoilState(sidebarOpenAtom); + const [videoCompletion , setVideoCompletion] = useRecoilState(videoCompletionAtom); + + useEffect(() => { + setVideoCompletion(convertToVideoCompletionData(fullCourseContent)); + } , []); useEffect(() => { if (window.innerWidth < 500) { @@ -101,7 +107,7 @@ export function Sidebar({ {content.type === 'video' ? (
- +
) : null} @@ -232,18 +238,23 @@ function NotionIcon() { } // Todo: Fix types here -function Check({ content }: { content: any }) { - const [completed, setCompleted] = useState( - content?.videoProgress?.markAsCompleted || false, - ); +function Check({ content , videoCompletion , setVideoCompletion }: { content: any , videoCompletion : VideoCompletionData[] , setVideoCompletion : any }) { + const completed = videoCompletion.length > 0 ? videoCompletion.find((item) => item.id === content.id)?.isCompleted : false; + return ( <> {}} onClick={async (e) => { - setCompleted(!completed); - handleMarkAsCompleted(!completed, content.id); e.stopPropagation(); + await handleMarkAsCompleted(!completed, content.id); + setVideoCompletion(videoCompletion.map((item : VideoCompletionData) => { + if (item.id === content.id) { + return {...item , isCompleted: !completed}; + } + return item; + })); }} type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" diff --git a/src/components/admin/ContentRendererClient.tsx b/src/components/admin/ContentRendererClient.tsx index 3dd40d353..9827c580a 100644 --- a/src/components/admin/ContentRendererClient.tsx +++ b/src/components/admin/ContentRendererClient.tsx @@ -4,7 +4,9 @@ import { useSearchParams, useRouter } from 'next/navigation'; import { VideoPlayerSegment } from '@/components/VideoPlayerSegment'; import VideoContentChapters from '../VideoContentChapters'; import { useState } from 'react'; -import { handleMarkAsCompleted } from '@/lib/utils'; +import { VideoCompletionData, handleMarkAsCompleted } from '@/lib/utils'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { videoCompletionAtom } from '@/store/atoms/videoCompletion'; export const ContentRendererClient = ({ metadata, @@ -26,10 +28,8 @@ export const ContentRendererClient = ({ markAsCompleted: boolean; }; }) => { - const [contentCompleted, setContentCompleted] = useState( - content.markAsCompleted, - ); - const [loadingMarkAs, setLoadingMarkAs] = useState(false); + const setVideoCompletion = useSetRecoilState(videoCompletionAtom); + const [showChapters, setShowChapters] = useState( metadata?.segments?.length > 0, ); @@ -77,17 +77,19 @@ export const ContentRendererClient = ({ setShowChapters((prev) => !prev); }; - const handleMarkCompleted = async () => { - setLoadingMarkAs(true); + const handleMarkCompleted = async (completed : boolean) => { const data: any = await handleMarkAsCompleted( - !contentCompleted, + completed, content.id, ); - if (data.contentId) { - setContentCompleted((prev) => !prev); + setVideoCompletion((prev) => prev.map((item : VideoCompletionData) => { + if (item.id === content.id) { + return {...item , isCompleted: data.markAsCompleted}; + } + return item; + })); } - setLoadingMarkAs(false); }; return ( @@ -118,7 +120,7 @@ export const ContentRendererClient = ({ sources: [source], }} onVideoEnd={() => { - setContentCompleted(true); + handleMarkCompleted(true); }} />
@@ -128,13 +130,7 @@ export const ContentRendererClient = ({ {content.title} - +
@@ -196,3 +192,20 @@ export const ContentRendererClient = ({
); }; + +const DisplayMarkAsComplete = ({contentId , handleMarkCompleted} : {contentId : number , handleMarkCompleted : any}) => { + const videoCompletion = useRecoilValue(videoCompletionAtom); + + const markAsComplete = videoCompletion.length > 0 ? videoCompletion.find((item) => item.id === contentId)?.isCompleted : false; + + return ( + + ); +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b63b64c8a..d8c2b9a05 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,5 @@ import { CommentFilter, QueryParams } from '@/actions/types'; +import { Folder } from '@/db/course'; import { CommentType, Prisma } from '@prisma/client'; import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -12,6 +13,40 @@ export interface VideoJsPlayer { eme: () => void; } +export const convertToVideoCompletionData = (currentCourse: Folder[]): VideoCompletionData[] => { + const videoCompletionData: VideoCompletionData[] = []; + + for (const folder of currentCourse) { + for (const child of folder?.children ?? []) { + if (child?.type === 'video') { + const { id, parentId, videoProgress } = child; + const isCompleted = videoProgress?.markAsCompleted ?? false; + videoCompletionData.push({ id, parentId, isCompleted }); + } + } + } + + return videoCompletionData; +}; + +export const getFolderContentCompleted = (videoCompletion : VideoCompletionData[], contentId : number) => { + const folderItems = videoCompletion.filter(item => item.parentId === contentId); + const totalContentInside = folderItems.length; + const totalCompleted = folderItems.filter(item => item.isCompleted).length; + + if (totalContentInside === 0) { + return 0; + } + + return Math.round((totalCompleted / totalContentInside) * 100); +}; + +export interface VideoCompletionData { + id : number; + parentId : number | null; + isCompleted : boolean | undefined; +} + export interface Segment { start: number; end: number; diff --git a/src/store/atoms/videoCompletion.ts b/src/store/atoms/videoCompletion.ts new file mode 100644 index 000000000..87aa4ad86 --- /dev/null +++ b/src/store/atoms/videoCompletion.ts @@ -0,0 +1,7 @@ +import { atom } from 'recoil'; +import { VideoCompletionData } from '@/lib/utils'; + +export const videoCompletionAtom = atom({ + key: 'videoCompletionAtom', + default: [] +}); \ No newline at end of file