diff --git a/src/components/content/courses/DocumentLesson.js b/src/components/content/courses/DocumentLesson.js
index 08e786e..5aa0379 100644
--- a/src/components/content/courses/DocumentLesson.js
+++ b/src/components/content/courses/DocumentLesson.js
@@ -11,6 +11,7 @@ import { getTotalFromZaps } from "@/utils/lightning";
import dynamic from "next/dynamic";
import useWindowWidth from "@/hooks/useWindowWidth";
import appConfig from "@/config/appConfig";
+import useTrackDocumentLesson from "@/hooks/tracking/useTrackDocumentLesson";
const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"),
@@ -22,10 +23,19 @@ const MDDisplay = dynamic(
const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
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();
const isMobileView = windowWidth <= 768;
+ // todo implement real read time needs to be on form
+ const readTime = 30;
+
+ const { isCompleted, isTracking } = useTrackDocumentLesson({
+ lessonId: lesson?.d,
+ courseId: course?.d,
+ readTime: readTime,
+ });
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
@@ -45,6 +55,12 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
}
}, [lesson]);
+ useEffect(() => {
+ if (isCompleted) {
+ setCompleted(lesson.id);
+ }
+ }, [isCompleted, lesson.id, setCompleted]);
+
const renderContent = () => {
if (isPaid && decryptionPerformed) {
return ;
@@ -90,14 +106,14 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
)}
-
{lesson.summary && (
+
{lesson.summary && (
{lesson.summary.split('\n').map((line, index) => (
{line}
))}
)}
-
+
['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()],
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
...(draft?.additionalLinks ? draft.additionalLinks.filter(link => link !== 'https://plebdevs.com').map(link => ['r', link]) : []),
+ draft?.price ? null : ['i', 'youtube:plebdevs', 'V_fvmyJ91m0']
];
type = 'video';
diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js
index b526049..d776044 100644
--- a/src/components/content/courses/VideoLesson.js
+++ b/src/components/content/courses/VideoLesson.js
@@ -38,17 +38,53 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
videoPlayed
});
+ useEffect(() => {
+ const handleYouTubeMessage = (event) => {
+ if (event.origin !== "https://www.youtube.com") return;
+
+ try {
+ const data = JSON.parse(event.data);
+ console.log('youtube data', data);
+ if (data.event === "onReady") {
+ event.source.postMessage('{"event":"listening"}', "https://www.youtube.com");
+ } else if (data.event === "infoDelivery" && data?.info && data?.info?.currentTime) {
+ setVideoPlayed(true);
+ setVideoDuration(data.info?.progressState?.duration);
+ event.source.postMessage('{"event":"command","func":"getDuration","args":""}', "https://www.youtube.com");
+ }
+ } catch (error) {
+ console.error("Error parsing YouTube message:", error);
+ }
+ };
+
+ window.addEventListener("message", handleYouTubeMessage);
+
+ return () => {
+ window.removeEventListener("message", handleYouTubeMessage);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (videoDuration && videoPlayed) {
+ console.log('videoDuration and videoPlayed', videoDuration, videoPlayed);
+ }
+ }, [videoDuration, videoPlayed]);
+
+ useEffect(() => {
+ if (videoPlayed) {
+ console.log('videoPlayed', videoPlayed);
+ }
+ }, [videoPlayed]);
+
const checkDuration = useCallback(() => {
const videoElement = mdDisplayRef.current?.querySelector('video');
+ const youtubeIframe = mdDisplayRef.current?.querySelector('iframe[src*="youtube.com"]');
+
if (videoElement && videoElement.readyState >= 1) {
setVideoDuration(Math.round(videoElement.duration));
-
- // Add event listener for play event
- videoElement.addEventListener('play', () => {
- setVideoPlayed(true);
- });
- } else if (videoElement) {
- setTimeout(checkDuration, 100);
+ setVideoPlayed(true);
+ } else if (youtubeIframe) {
+ youtubeIframe.contentWindow.postMessage('{"event":"listening"}', '*');
}
}, []);
@@ -78,6 +114,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
if (decryptionPerformed && isPaid) {
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);
}
}, [decryptionPerformed, isPaid, checkDuration]);
@@ -109,7 +149,11 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
);
} else if (lesson?.content) {
- return
;
+ return (
+
+
+
+ );
}
return null;
}
@@ -129,14 +173,14 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
)}
-
{lesson.summary && (
+
{lesson.summary && (
{lesson.summary.split('\n').map((line, index) => (
{line}
))}
)}
-
+
{
const [title, setTitle] = useState(draft?.title || '');
const [summary, setSummary] = useState(draft?.summary || '');
@@ -53,7 +54,7 @@ const VideoForm = ({ draft = null }) => {
// Check if it's a YouTube video
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
- embedCode = ``;
+ embedCode = ``;
}
// Check if it's a Vimeo video
else if (videoUrl.includes('vimeo.com')) {
diff --git a/src/components/forms/course/embedded/EmbeddedVideoForm.js b/src/components/forms/course/embedded/EmbeddedVideoForm.js
index 0e0ed5f..b50f16a 100644
--- a/src/components/forms/course/embedded/EmbeddedVideoForm.js
+++ b/src/components/forms/course/embedded/EmbeddedVideoForm.js
@@ -50,7 +50,7 @@ const EmbeddedVideoForm = ({ draft = null, onSave, isPaid }) => {
// Check if it's a YouTube video
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
- embedCode = ``;
+ embedCode = ``;
}
// Check if it's a Vimeo video
else if (videoUrl.includes('vimeo.com')) {
diff --git a/src/hooks/tracking/useTrackDocumentLesson.js b/src/hooks/tracking/useTrackDocumentLesson.js
new file mode 100644
index 0000000..1e47dd3
--- /dev/null
+++ b/src/hooks/tracking/useTrackDocumentLesson.js
@@ -0,0 +1,105 @@
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { useSession } from 'next-auth/react';
+import axios from 'axios';
+
+const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
+ 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 completedRef = useRef(false);
+
+ useEffect(() => {
+ if (session?.user?.role?.admin) {
+ setIsAdmin(true);
+ setIsCompleted(true); // Automatically mark as completed for admins
+ }
+ }, [session]);
+
+ 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) {
+ if (response?.data?.completed) {
+ setIsCompleted(true);
+ completedRef.current = true;
+ return true;
+ } else {
+ 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(),
+ });
+ return false;
+ } else {
+ console.error('Error checking or creating UserLesson:', response.statusText);
+ return false;
+ }
+ } catch (error) {
+ console.error('Error checking or creating UserLesson:', error);
+ return false;
+ }
+ }, [session, lessonId, courseId]);
+
+ const markLessonAsCompleted = useCallback(async () => {
+ if (!session?.user || completedRef.current) return;
+ completedRef.current = true;
+
+ try {
+ const response = await axios.put(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`, {
+ completed: true,
+ completedAt: new Date().toISOString(),
+ });
+
+ if (response.status === 200) {
+ setIsCompleted(true);
+ setIsTracking(false);
+ } else {
+ console.error('Failed to mark lesson as completed:', response.statusText);
+ }
+ } catch (error) {
+ console.error('Error marking lesson as completed:', error);
+ }
+ }, [lessonId, courseId, session]);
+
+ useEffect(() => {
+ const initializeTracking = async () => {
+ if (isAdmin) return; // Skip tracking for admin users
+
+ const alreadyCompleted = await checkOrCreateUserLesson();
+ if (!alreadyCompleted && !completedRef.current) {
+ setIsTracking(true);
+ timerRef.current = setInterval(() => {
+ setTimeSpent(prevTime => prevTime + 1);
+ }, 1000);
+ }
+ };
+
+ initializeTracking();
+
+ return () => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ }
+ };
+ }, [lessonId, checkOrCreateUserLesson, isAdmin]);
+
+ useEffect(() => {
+ if (isAdmin) return; // Skip tracking for admin users
+
+ // Mark lesson as completed after readTime seconds
+ if (timeSpent >= readTime && !completedRef.current) {
+ markLessonAsCompleted();
+ }
+ }, [timeSpent, markLessonAsCompleted, readTime, isAdmin]);
+
+ return { isCompleted, isTracking };
+};
+
+export default useTrackDocumentLesson;
\ No newline at end of file
diff --git a/src/hooks/tracking/useTrackVideoLesson.js b/src/hooks/tracking/useTrackVideoLesson.js
index 143f78a..bbfc8fe 100644
--- a/src/hooks/tracking/useTrackVideoLesson.js
+++ b/src/hooks/tracking/useTrackVideoLesson.js
@@ -6,10 +6,18 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
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 completedRef = useRef(false);
+ useEffect(() => {
+ if (session?.user?.role?.admin) {
+ setIsAdmin(true);
+ setIsCompleted(true); // Automatically mark as completed for admins
+ }
+ }, [session]);
+
// Check if the lesson is already completed or create a new UserLesson record
const checkOrCreateUserLesson = useCallback(async () => {
if (!session?.user) return false;
@@ -68,6 +76,9 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
useEffect(() => {
const initializeTracking = async () => {
+ console.log('initializeTracking', videoDuration, !completedRef.current, videoPlayed);
+ if (isAdmin) return; // Skip tracking for admin users
+
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) {
@@ -87,14 +98,16 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
clearInterval(timerRef.current);
}
};
- }, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed]);
+ }, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin]);
useEffect(() => {
+ if (isAdmin) return; // Skip tracking for admin users
+
// Case 4: Mark lesson as completed when 90% of the video is watched
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
markLessonAsCompleted();
}
- }, [timeSpent, videoDuration, markLessonAsCompleted]);
+ }, [timeSpent, videoDuration, markLessonAsCompleted, isAdmin]);
return { isCompleted, isTracking };
};
diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js
index 5ed1eef..c3a93f2 100644
--- a/src/pages/api/auth/[...nextauth].js
+++ b/src/pages/api/auth/[...nextauth].js
@@ -106,6 +106,7 @@ export const authOptions = {
}
}
+ // todo this does not work on first login only the second time
if (user && appConfig.authorPubkeys.includes(user?.pubkey) && !user?.role) {
// create a new author role for this user
const role = await createRole({