fix course and lesson tracking for paid courses, add usercourses and userlessons into session

This commit is contained in:
austinkelsay 2024-09-22 17:08:26 -05:00
parent eb1b8675b9
commit 215a00e593
10 changed files with 117 additions and 48 deletions

View File

@ -34,7 +34,11 @@ export default function CourseDetailsNew({ processedEvent, paidCourse, lessons,
const isMobileView = windowWidth <= 768;
const { ndk } = useNDKContext();
const { isCompleted } = useTrackCourse({courseId: processedEvent?.d});
const { isCompleted } = useTrackCourse({
courseId: processedEvent?.d,
paidCourse,
decryptionPerformed
});
const fetchAuthor = useCallback(async (pubkey) => {
if (!pubkey) return;

View File

@ -34,6 +34,8 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
lessonId: lesson?.d,
courseId: course?.d,
readTime: readTime,
paidCourse: isPaid,
decryptionPerformed: decryptionPerformed,
});
useEffect(() => {

View File

@ -35,7 +35,9 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
lessonId: lesson?.d,
videoDuration,
courseId: course?.d,
videoPlayed
videoPlayed,
paidCourse: isPaid,
decryptionPerformed
});
useEffect(() => {

View File

@ -7,11 +7,21 @@ const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
export const getAllUsers = async () => {
return await prisma.user.findMany({
include: {
role: true, // Include related role
role: true,
purchased: {
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
course: true,
resource: true,
},
},
userCourses: {
include: {
course: true,
},
},
userLessons: {
include: {
lesson: true,
},
},
},
@ -22,11 +32,21 @@ export const getUserById = async (id) => {
return await prisma.user.findUnique({
where: { id },
include: {
role: true, // Include related role
role: true,
purchased: {
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
course: true,
resource: true,
},
},
userCourses: {
include: {
course: true,
},
},
userLessons: {
include: {
lesson: true,
},
},
},
@ -37,11 +57,21 @@ export const getUserByPubkey = async (pubkey) => {
return await prisma.user.findUnique({
where: { pubkey },
include: {
role: true, // Include related role
role: true,
purchased: {
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
course: true,
resource: true,
},
},
userCourses: {
include: {
course: true,
},
},
userLessons: {
include: {
lesson: true,
},
},
},

View File

@ -2,24 +2,30 @@ import React, {useState, useEffect, useRef, useCallback} from 'react';
import {useSession} from 'next-auth/react';
import axios from 'axios';
const useTrackCourse = ({courseId}) => {
const useTrackCourse = ({courseId, paidCourse, decryptionPerformed}) => {
const [isCompleted, setIsCompleted] = useState(false);
const {data: session} = useSession();
const {data: session, update} = useSession();
const completedRef = useRef(false);
const checkOrCreateUserCourse = useCallback(async () => {
if (!session?.user) return false;
if (!session?.user || !courseId) 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(),
});
// Only create a new UserCourse entry if it's a free course or if decryption has been performed for a paid course
if (paidCourse === false || (paidCourse && decryptionPerformed)) {
console.log("creating new UserCourse entry");
await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, {
completed: false,
started: true,
startedAt: new Date().toISOString(),
});
// Call session update after creating a new UserCourse entry
await update();
}
setIsCompleted(false);
return false;
@ -31,13 +37,13 @@ const useTrackCourse = ({courseId}) => {
console.error('Error checking or creating UserCourse:', error);
return false;
}
}, [session, courseId]);
}, [courseId, paidCourse, decryptionPerformed]);
useEffect(() => {
if (!completedRef.current && courseId) {
if (!completedRef.current && courseId && session?.user) {
checkOrCreateUserCourse();
}
}, [checkOrCreateUserCourse, courseId]);
}, [courseId]);
return {isCompleted};
};

View File

@ -2,13 +2,13 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import axios from 'axios';
const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
const useTrackDocumentLesson = ({ lessonId, courseId, readTime, paidCourse, decryptionPerformed }) => {
const [isCompleted, setIsCompleted] = useState(false);
const [timeSpent, setTimeSpent] = useState(0);
const [isTracking, setIsTracking] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const timerRef = useRef(null);
const { data: session } = useSession();
const { data: session, update } = useSession();
const completedRef = useRef(false);
useEffect(() => {
@ -31,11 +31,16 @@ const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
return false;
}
} else if (response.status === 204) {
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
resourceId: lessonId,
opened: true,
openedAt: new Date().toISOString(),
});
// Only create a new UserLesson entry if it's a free course or if decryption has been performed for a paid course
if (paidCourse === false || (paidCourse && decryptionPerformed)) {
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
resourceId: lessonId,
opened: true,
openedAt: new Date().toISOString(),
});
// Call session update after creating a new UserLesson entry
await update();
}
return false;
} else {
console.error('Error checking or creating UserLesson:', response.statusText);
@ -45,7 +50,7 @@ const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
console.error('Error checking or creating UserLesson:', error);
return false;
}
}, [session, lessonId, courseId]);
}, [session, lessonId, courseId, update, paidCourse, decryptionPerformed]);
const markLessonAsCompleted = useCallback(async () => {
if (!session?.user || completedRef.current) return;
@ -60,20 +65,22 @@ const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
if (response.status === 200) {
setIsCompleted(true);
setIsTracking(false);
// Call session update after marking the lesson as completed
await update();
} else {
console.error('Failed to mark lesson as completed:', response.statusText);
}
} catch (error) {
console.error('Error marking lesson as completed:', error);
}
}, [lessonId, courseId, session]);
}, [lessonId, courseId, session, update]);
useEffect(() => {
const initializeTracking = async () => {
if (isAdmin) return; // Skip tracking for admin users
const alreadyCompleted = await checkOrCreateUserLesson();
if (!alreadyCompleted && !completedRef.current) {
if (!alreadyCompleted && !completedRef.current && (!paidCourse || (paidCourse && decryptionPerformed))) {
setIsTracking(true);
timerRef.current = setInterval(() => {
setTimeSpent(prevTime => prevTime + 1);
@ -88,7 +95,7 @@ const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
clearInterval(timerRef.current);
}
};
}, [lessonId, checkOrCreateUserLesson, isAdmin]);
}, [lessonId, checkOrCreateUserLesson, isAdmin, paidCourse, decryptionPerformed]);
useEffect(() => {
if (isAdmin) return; // Skip tracking for admin users

View File

@ -2,13 +2,13 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import axios from 'axios';
const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) => {
const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed, paidCourse, decryptionPerformed}) => {
const [isCompleted, setIsCompleted] = useState(false);
const [timeSpent, setTimeSpent] = useState(0);
const [isTracking, setIsTracking] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const timerRef = useRef(null);
const { data: session } = useSession();
const { data: session, update } = useSession();
const completedRef = useRef(false);
useEffect(() => {
@ -31,11 +31,16 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
return false;
}
} else if (response.status === 204) {
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
resourceId: lessonId,
opened: true,
openedAt: new Date().toISOString(),
});
// Only create a new UserLesson entry if it's a free course or if decryption has been performed for a paid course
if (paidCourse === false || (paidCourse && decryptionPerformed)) {
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
resourceId: lessonId,
opened: true,
openedAt: new Date().toISOString(),
});
// Call session update after creating a new UserLesson entry
await update();
}
return false;
} else {
console.error('Error checking or creating UserLesson:', response.statusText);
@ -45,7 +50,7 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
console.error('Error checking or creating UserLesson:', error);
return false;
}
}, [session, lessonId, courseId]);
}, [session, lessonId, courseId, update, paidCourse, decryptionPerformed]);
const markLessonAsCompleted = useCallback(async () => {
if (!session?.user || completedRef.current) return;
@ -60,20 +65,22 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
if (response.status === 200) {
setIsCompleted(true);
setIsTracking(false);
// Call session update after marking the lesson as completed
await update();
} else {
console.error('Failed to mark lesson as completed:', response.statusText);
}
} catch (error) {
console.error('Error marking lesson as completed:', error);
}
}, [lessonId, courseId, session]);
}, [lessonId, courseId, session, update]);
useEffect(() => {
const initializeTracking = async () => {
if (isAdmin) return;
const alreadyCompleted = await checkOrCreateUserLesson();
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed && (paidCourse === false || (paidCourse && decryptionPerformed))) {
console.log(`Tracking started for lesson ${lessonId}, video duration: ${videoDuration} seconds, video played: ${videoPlayed}`);
setIsTracking(true);
timerRef.current = setInterval(() => {
@ -89,7 +96,7 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
clearInterval(timerRef.current);
}
};
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin]);
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin, paidCourse, decryptionPerformed]);
useEffect(() => {
if (isAdmin) return;

View File

@ -1,5 +1,6 @@
import { getServerSession } from "next-auth/next"
import { authOptions } from "./auth/[...nextauth]"
import { getLessonsByCourseId } from "@/db/models/lessonModels"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import appConfig from "@/config/appConfig";
@ -35,9 +36,18 @@ export default async function handler(req, res) {
// Check if the user is authorized to access the video
if (!session.user.role?.subscribed && !appConfig.authorPubkeys.includes(session.user.pubkey)) {
const purchasedVideo = session.user.purchased?.find(purchase => purchase?.resource?.videoId === videoKey)
console.log("purchasedVideo", purchasedVideo)
// first check if it is individual video
if (!purchasedVideo) {
return res.status(403).json({ error: "Forbidden: You don't have access to this video" })
// next we have to check if it is in a course the user has purchased
const allPurchasedCourses = session?.user?.purchased?.filter(purchase => purchase?.courseId) || []
const allPurchasedLessons = await Promise.all(
allPurchasedCourses.map(course => getLessonsByCourseId(course.courseId))
).then(lessonsArrays => lessonsArrays.flat())
if (!allPurchasedLessons.some(lesson => lesson?.resource?.videoId === videoKey)) {
return res.status(403).json({ error: "Forbidden: You don't have access to this video" })
}
}
}

View File

@ -12,8 +12,9 @@ export default async function handler(req, res) {
// If slug is a pubkey
user = await getUserByPubkey(slug);
} else if (isEmail) {
// todo
// If slug is an email
user = await getUserByEmail(slug);
// user = await getUserByEmail(slug);
} else {
// Assume slug is an ID
const id = parseInt(slug);

View File

@ -139,7 +139,7 @@ const Course = () => {
const { ndk, addSigner } = useNDKContext();
const { data: session, update } = useSession();
const { showToast } = useToast();
const [paidCourse, setPaidCourse] = useState(false);
const [paidCourse, setPaidCourse] = useState(null);
const [expandedIndex, setExpandedIndex] = useState(null);
const [completedLessons, setCompletedLessons] = useState([]);