mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
fix course and lesson tracking for paid courses, add usercourses and userlessons into session
This commit is contained in:
parent
eb1b8675b9
commit
215a00e593
@ -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;
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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};
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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([]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user