diff --git a/src/components/content/courses/DocumentLesson.js b/src/components/content/courses/DocumentLesson.js
index 5aa0379..f1982aa 100644
--- a/src/components/content/courses/DocumentLesson.js
+++ b/src/components/content/courses/DocumentLesson.js
@@ -20,10 +20,9 @@ const MDDisplay = dynamic(
}
);
-const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
+const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => {
const [zapAmount, setZapAmount] = useState(0);
const [nAddress, setNAddress] = useState(null);
- const [completed, setCompleted] = useState(false);
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth();
@@ -56,10 +55,10 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
}, [lesson]);
useEffect(() => {
- if (isCompleted) {
+ if (isCompleted && !isTracking) {
setCompleted(lesson.id);
}
- }, [isCompleted, lesson.id, setCompleted]);
+ }, [isCompleted, lesson.id, setCompleted, isTracking]);
const renderContent = () => {
if (isPaid && decryptionPerformed) {
diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js
index d776044..770f081 100644
--- a/src/components/content/courses/VideoLesson.js
+++ b/src/components/content/courses/VideoLesson.js
@@ -89,10 +89,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
}, []);
useEffect(() => {
- if (isCompleted) {
+ if (isCompleted && !isTracking) {
setCompleted(lesson.id);
}
- }, [isCompleted, lesson.id]); // Remove setCompleted from dependencies
+ }, [isCompleted, lesson.id, setCompleted, isTracking]);
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
@@ -115,7 +115,6 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
const timer = setTimeout(checkDuration, 500);
return () => clearTimeout(timer);
} else {
- // For non-paid content, start checking after 3 seconds
const timer = setTimeout(checkDuration, 3000);
return () => clearTimeout(timer);
}
diff --git a/src/db/models/userCourseModels.js b/src/db/models/userCourseModels.js
new file mode 100644
index 0000000..6c0c418
--- /dev/null
+++ b/src/db/models/userCourseModels.js
@@ -0,0 +1,84 @@
+import prisma from "@/db/prisma";
+
+export const getUserCourses = async (userId) => {
+ return await prisma.userCourse.findMany({
+ where: { userId },
+ include: { course: true },
+ });
+};
+
+export const getUserCourse = async (userId, courseId) => {
+ return await prisma.userCourse.findUnique({
+ where: {
+ userId_courseId: {
+ userId,
+ courseId,
+ },
+ },
+ include: { course: true },
+ });
+};
+
+export const createOrUpdateUserCourse = async (userId, courseId, data) => {
+ return await prisma.userCourse.upsert({
+ where: {
+ userId_courseId: {
+ userId,
+ courseId,
+ },
+ },
+ update: {
+ ...data,
+ updatedAt: new Date(),
+ },
+ create: {
+ userId,
+ courseId,
+ ...data,
+ },
+ });
+};
+
+export const deleteUserCourse = async (userId, courseId) => {
+ return await prisma.userCourse.delete({
+ where: {
+ userId_courseId: {
+ userId,
+ courseId,
+ },
+ },
+ });
+};
+
+export const checkCourseCompletion = async (userId, courseId) => {
+ const course = await prisma.course.findUnique({
+ where: { id: courseId },
+ include: {
+ lessons: {
+ include: {
+ userLessons: {
+ where: { userId: userId }
+ }
+ }
+ }
+ }
+ });
+
+ if (!course) {
+ throw new Error("Course not found");
+ }
+
+ const allLessonsCompleted = course.lessons.every(lesson =>
+ lesson.userLessons.length > 0 && lesson.userLessons[0].completed
+ );
+
+ if (allLessonsCompleted) {
+ await createOrUpdateUserCourse(userId, courseId, {
+ completed: true,
+ completedAt: new Date()
+ });
+ return true;
+ }
+
+ return false;
+};
\ No newline at end of file
diff --git a/src/db/models/userLessonModels.js b/src/db/models/userLessonModels.js
index 6613744..e96a2dc 100644
--- a/src/db/models/userLessonModels.js
+++ b/src/db/models/userLessonModels.js
@@ -20,7 +20,6 @@ export const getUserLesson = async (userId, lessonId) => {
};
export const createOrUpdateUserLesson = async (userId, lessonId, data) => {
- console.log(`Creating or updating user lesson for user ${userId} and lesson ${lessonId} with data:`, data);
return await prisma.userLesson.upsert({
where: {
userId_lessonId: {
diff --git a/src/hooks/tracking/useTrackCourse.js b/src/hooks/tracking/useTrackCourse.js
new file mode 100644
index 0000000..0595a07
--- /dev/null
+++ b/src/hooks/tracking/useTrackCourse.js
@@ -0,0 +1,45 @@
+import React, {useState, useEffect, useRef, useCallback} from 'react';
+import {useSession} from 'next-auth/react';
+import axios from 'axios';
+
+const useTrackCourse = ({courseId}) => {
+ const [isCompleted, setIsCompleted] = useState(false);
+ const {data: session} = useSession();
+ const completedRef = useRef(false);
+
+ const checkOrCreateUserCourse = useCallback(async () => {
+ if (!session?.user) return false;
+ try {
+ const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`);
+ if (response.status === 200 && response?.data) {
+ setIsCompleted(true);
+ completedRef.current = true;
+ } else if (response.status === 204) {
+ await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, {
+ completed: false,
+ started: true,
+ startedAt: new Date().toISOString(),
+ });
+
+ setIsCompleted(false);
+ return false;
+ } else {
+ console.error('Error checking or creating UserCourse:', response.statusText);
+ return false;
+ }
+ } catch (error) {
+ console.error('Error checking or creating UserCourse:', error);
+ return false;
+ }
+ }, [session, courseId]);
+
+ useEffect(() => {
+ if (!completedRef.current && courseId) {
+ checkOrCreateUserCourse();
+ }
+ }, [checkOrCreateUserCourse, courseId]);
+
+ return {isCompleted};
+};
+
+export default useTrackCourse;
\ No newline at end of file
diff --git a/src/hooks/tracking/useTrackVideoLesson.js b/src/hooks/tracking/useTrackVideoLesson.js
index bbfc8fe..4d3ee50 100644
--- a/src/hooks/tracking/useTrackVideoLesson.js
+++ b/src/hooks/tracking/useTrackVideoLesson.js
@@ -18,26 +18,20 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
}
}, [session]);
- // Check if the lesson is already completed or create a new UserLesson record
const checkOrCreateUserLesson = useCallback(async () => {
if (!session?.user) return false;
try {
const response = await axios.get(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`);
if (response.status === 200 && response?.data) {
- // Case 1: UserLesson record exists
if (response?.data?.completed) {
- // Lesson is already completed
setIsCompleted(true);
completedRef.current = true;
return true;
} else {
- // Lesson exists but is not completed
return false;
}
} else if (response.status === 204) {
- // Case 2: UserLesson record doesn't exist, create a new one
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
- // currently the only id we get is the resource id which associates to the lesson
resourceId: lessonId,
opened: true,
openedAt: new Date().toISOString(),
@@ -51,7 +45,7 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
console.error('Error checking or creating UserLesson:', error);
return false;
}
- }, [session, lessonId]);
+ }, [session, lessonId, courseId]);
const markLessonAsCompleted = useCallback(async () => {
if (!session?.user || completedRef.current) return;
@@ -72,15 +66,13 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
} catch (error) {
console.error('Error marking lesson as completed:', error);
}
- }, [lessonId, session]);
+ }, [lessonId, courseId, session]);
useEffect(() => {
const initializeTracking = async () => {
- console.log('initializeTracking', videoDuration, !completedRef.current, videoPlayed);
- if (isAdmin) return; // Skip tracking for admin users
+ if (isAdmin) return;
const alreadyCompleted = await checkOrCreateUserLesson();
- // Case 3: Start tracking if the lesson is not completed, video duration is available, and video has been played
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
console.log(`Tracking started for lesson ${lessonId}, video duration: ${videoDuration} seconds, video played: ${videoPlayed}`);
setIsTracking(true);
@@ -92,7 +84,6 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
initializeTracking();
- // Cleanup function to clear the interval when the component unmounts
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
@@ -101,9 +92,8 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin]);
useEffect(() => {
- if (isAdmin) return; // Skip tracking for admin users
+ if (isAdmin) return;
- // Case 4: Mark lesson as completed when 90% of the video is watched
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
markLessonAsCompleted();
}
diff --git a/src/pages/api/users/[slug]/courses/[courseSlug].js b/src/pages/api/users/[slug]/courses/[courseSlug].js
new file mode 100644
index 0000000..6ec5572
--- /dev/null
+++ b/src/pages/api/users/[slug]/courses/[courseSlug].js
@@ -0,0 +1,25 @@
+import { checkCourseCompletion } from "@/db/models/userCourseModels";
+
+// todo somehow make it to where we can get lesson slug in this endpoint
+export default async function handler(req, res) {
+ const { method } = req;
+ const { slug, courseSlug } = req.query;
+ switch (method) {
+ case "GET":
+ try {
+ const courseCompletion = await checkCourseCompletion(slug, courseSlug);
+ if (courseCompletion) {
+ res.status(200).json(courseCompletion);
+ } else {
+ res.status(204).end();
+ }
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+ break;
+
+ default:
+ res.setHeader("Allow", ["GET", "PUT", "DELETE"]);
+ res.status(405).end(`Method ${method} Not Allowed`);
+ }
+}
\ No newline at end of file
diff --git a/src/pages/api/users/[slug]/courses/index.js b/src/pages/api/users/[slug]/courses/index.js
new file mode 100644
index 0000000..086a2d9
--- /dev/null
+++ b/src/pages/api/users/[slug]/courses/index.js
@@ -0,0 +1,21 @@
+import { createOrUpdateUserCourse } from "@/db/models/userCourseModels";
+
+export default async function handler(req, res) {
+ const { method } = req;
+ const { slug, courseSlug } = req.query;
+ const userId = slug;
+ switch (method) {
+ case "POST":
+ try {
+ const userCourse = await createOrUpdateUserCourse(userId, courseSlug, req.body);
+ res.status(201).json(userCourse);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+ break;
+
+ default:
+ res.setHeader("Allow", ["GET", "POST"]);
+ res.status(405).end(`Method ${method} Not Allowed`);
+ }
+}
\ No newline at end of file
diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js
index 170a118..3d9eb17 100644
--- a/src/pages/course/[slug]/index.js
+++ b/src/pages/course/[slug]/index.js
@@ -143,10 +143,10 @@ const Course = () => {
const [expandedIndex, setExpandedIndex] = useState(null);
const [completedLessons, setCompletedLessons] = useState([]);
- const setCompleted = (lessonId) => {
+ const setCompleted = useCallback((lessonId) => {
console.log('setting completed', lessonId);
setCompletedLessons(prev => [...prev, lessonId]);
- }
+ }, []);
const fetchAuthor = useCallback(async (pubkey) => {
const author = await ndk.getUser({ pubkey });
@@ -247,7 +247,7 @@ const Course = () => {
{lesson.type === 'video' ?
:
-
+
}