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 isMobileView = windowWidth <= 768;
const { ndk } = useNDKContext(); const { ndk } = useNDKContext();
const { isCompleted } = useTrackCourse({courseId: processedEvent?.d}); const { isCompleted } = useTrackCourse({
courseId: processedEvent?.d,
paidCourse,
decryptionPerformed
});
const fetchAuthor = useCallback(async (pubkey) => { const fetchAuthor = useCallback(async (pubkey) => {
if (!pubkey) return; if (!pubkey) return;

View File

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

View File

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

View File

@ -7,11 +7,21 @@ const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
export const getAllUsers = async () => { export const getAllUsers = async () => {
return await prisma.user.findMany({ return await prisma.user.findMany({
include: { include: {
role: true, // Include related role role: true,
purchased: { purchased: {
include: { include: {
course: true, // Include course details in purchases course: true,
resource: true, // Include resource details in purchases resource: true,
},
},
userCourses: {
include: {
course: true,
},
},
userLessons: {
include: {
lesson: true,
}, },
}, },
}, },
@ -22,11 +32,21 @@ export const getUserById = async (id) => {
return await prisma.user.findUnique({ return await prisma.user.findUnique({
where: { id }, where: { id },
include: { include: {
role: true, // Include related role role: true,
purchased: { purchased: {
include: { include: {
course: true, // Include course details in purchases course: true,
resource: true, // Include resource details in purchases resource: true,
},
},
userCourses: {
include: {
course: true,
},
},
userLessons: {
include: {
lesson: true,
}, },
}, },
}, },
@ -37,11 +57,21 @@ export const getUserByPubkey = async (pubkey) => {
return await prisma.user.findUnique({ return await prisma.user.findUnique({
where: { pubkey }, where: { pubkey },
include: { include: {
role: true, // Include related role role: true,
purchased: { purchased: {
include: { include: {
course: true, // Include course details in purchases course: true,
resource: true, // Include resource details in purchases 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 {useSession} from 'next-auth/react';
import axios from 'axios'; import axios from 'axios';
const useTrackCourse = ({courseId}) => { const useTrackCourse = ({courseId, paidCourse, decryptionPerformed}) => {
const [isCompleted, setIsCompleted] = useState(false); const [isCompleted, setIsCompleted] = useState(false);
const {data: session} = useSession(); const {data: session, update} = useSession();
const completedRef = useRef(false); const completedRef = useRef(false);
const checkOrCreateUserCourse = useCallback(async () => { const checkOrCreateUserCourse = useCallback(async () => {
if (!session?.user) return false; if (!session?.user || !courseId) return false;
try { try {
const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`); const response = await axios.get(`/api/users/${session.user.id}/courses/${courseId}`);
if (response.status === 200 && response?.data) { if (response.status === 200 && response?.data) {
setIsCompleted(true); setIsCompleted(true);
completedRef.current = true; completedRef.current = true;
} else if (response.status === 204) { } else if (response.status === 204) {
await axios.post(`/api/users/${session.user.id}/courses?courseSlug=${courseId}`, { // Only create a new UserCourse entry if it's a free course or if decryption has been performed for a paid course
completed: false, if (paidCourse === false || (paidCourse && decryptionPerformed)) {
started: true, console.log("creating new UserCourse entry");
startedAt: new Date().toISOString(), 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); setIsCompleted(false);
return false; return false;
@ -31,13 +37,13 @@ const useTrackCourse = ({courseId}) => {
console.error('Error checking or creating UserCourse:', error); console.error('Error checking or creating UserCourse:', error);
return false; return false;
} }
}, [session, courseId]); }, [courseId, paidCourse, decryptionPerformed]);
useEffect(() => { useEffect(() => {
if (!completedRef.current && courseId) { if (!completedRef.current && courseId && session?.user) {
checkOrCreateUserCourse(); checkOrCreateUserCourse();
} }
}, [checkOrCreateUserCourse, courseId]); }, [courseId]);
return {isCompleted}; return {isCompleted};
}; };

View File

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

View File

@ -1,5 +1,6 @@
import { getServerSession } from "next-auth/next" import { getServerSession } from "next-auth/next"
import { authOptions } from "./auth/[...nextauth]" import { authOptions } from "./auth/[...nextauth]"
import { getLessonsByCourseId } from "@/db/models/lessonModels"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3" import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import appConfig from "@/config/appConfig"; 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 // Check if the user is authorized to access the video
if (!session.user.role?.subscribed && !appConfig.authorPubkeys.includes(session.user.pubkey)) { if (!session.user.role?.subscribed && !appConfig.authorPubkeys.includes(session.user.pubkey)) {
const purchasedVideo = session.user.purchased?.find(purchase => purchase?.resource?.videoId === videoKey) const purchasedVideo = session.user.purchased?.find(purchase => purchase?.resource?.videoId === videoKey)
console.log("purchasedVideo", purchasedVideo) // first check if it is individual video
if (!purchasedVideo) { 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 // If slug is a pubkey
user = await getUserByPubkey(slug); user = await getUserByPubkey(slug);
} else if (isEmail) { } else if (isEmail) {
// todo
// If slug is an email // If slug is an email
user = await getUserByEmail(slug); // user = await getUserByEmail(slug);
} else { } else {
// Assume slug is an ID // Assume slug is an ID
const id = parseInt(slug); const id = parseInt(slug);

View File

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