diff --git a/src/components/content/courses/CombinedLesson.js b/src/components/content/courses/CombinedLesson.js new file mode 100644 index 0000000..d3ce060 --- /dev/null +++ b/src/components/content/courses/CombinedLesson.js @@ -0,0 +1,259 @@ +import React, { useEffect, useState, useRef, useCallback } from "react"; +import { Tag } from "primereact/tag"; +import Image from "next/image"; +import ZapDisplay from "@/components/zaps/ZapDisplay"; +import { useImageProxy } from "@/hooks/useImageProxy"; +import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; +import GenericButton from "@/components/buttons/GenericButton"; +import { nip19 } from "nostr-tools"; +import { Divider } from "primereact/divider"; +import { getTotalFromZaps } from "@/utils/lightning"; +import dynamic from "next/dynamic"; +import useWindowWidth from "@/hooks/useWindowWidth"; +import appConfig from "@/config/appConfig"; +import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson'; + +const MDDisplay = dynamic( + () => import("@uiw/react-markdown-preview"), + { + ssr: false, + } +); + +const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => { + const [zapAmount, setZapAmount] = useState(0); + const [nAddress, setNAddress] = useState(null); + const [videoDuration, setVideoDuration] = useState(null); + const [videoPlayed, setVideoPlayed] = useState(false); + const mdDisplayRef = useRef(null); + const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" }); + const { returnImageProxy } = useImageProxy(); + const windowWidth = useWindowWidth(); + const isMobileView = windowWidth <= 768; + const isVideo = lesson?.type === 'video'; + + const { isCompleted: videoCompleted, isTracking: videoTracking } = useTrackVideoLesson({ + lessonId: lesson?.d, + videoDuration, + courseId: course?.d, + videoPlayed, + paidCourse: isPaid, + decryptionPerformed + }); + + useEffect(() => { + const handleYouTubeMessage = (event) => { + if (event.origin !== "https://www.youtube.com") return; + + try { + const data = JSON.parse(event.data); + if (data.event === "onReady") { + event.source.postMessage('{"event":"listening"}', "https://www.youtube.com"); + } else if (data.event === "infoDelivery" && data?.info?.currentTime) { + setVideoPlayed(true); + setVideoDuration(data.info?.progressState?.duration); + } + } catch (error) { + console.error("Error parsing YouTube message:", error); + } + }; + + if (isVideo) { + window.addEventListener("message", handleYouTubeMessage); + return () => window.removeEventListener("message", handleYouTubeMessage); + } + }, [isVideo]); + + const checkDuration = useCallback(() => { + if (!isVideo) return; + + 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)); + setVideoPlayed(true); + } else if (youtubeIframe) { + youtubeIframe.contentWindow.postMessage('{"event":"listening"}', '*'); + } + }, [isVideo]); + + useEffect(() => { + if (!zaps || zapsLoading || zapsError) return; + const total = getTotalFromZaps(zaps, lesson); + setZapAmount(total); + }, [zaps, zapsLoading, zapsError, lesson]); + + useEffect(() => { + if (lesson) { + const addr = nip19.naddrEncode({ + pubkey: lesson.pubkey, + kind: lesson.kind, + identifier: lesson.d, + relays: appConfig.defaultRelayUrls + }); + setNAddress(addr); + } + }, [lesson]); + + useEffect(() => { + if (decryptionPerformed && isPaid) { + const timer = setTimeout(checkDuration, 500); + return () => clearTimeout(timer); + } else { + const timer = setTimeout(checkDuration, 3000); + return () => clearTimeout(timer); + } + }, [decryptionPerformed, isPaid, checkDuration]); + + useEffect(() => { + if (isVideo && videoCompleted && !videoTracking) { + setCompleted(lesson.id); + } + }, [videoCompleted, videoTracking, lesson.id, setCompleted, isVideo]); + + const renderContent = () => { + if (isPaid && decryptionPerformed) { + return ( +
+ +
+ ); + } + + if (isPaid && !decryptionPerformed) { + if (isVideo) { + return ( +
+
+
+
+ +
+

+ This content is paid and needs to be purchased before viewing. +

+
+ ); + } + return ( +
+
+ +
+

+ This content is paid and needs to be purchased before viewing. +

+
+ ); + } + + if (lesson?.content) { + return ( +
+ +
+ ); + } + + return null; + }; + + return ( +
+ {isVideo ? renderContent() : ( + <> +
+ lesson background image +
+
+ + )} +
+
+
+

{lesson.title}

+
+ {lesson.topics && lesson.topics.length > 0 && ( + lesson.topics.map((topic, index) => ( + + )) + )} +
+
+
{lesson.summary && ( +
+ {lesson.summary.split('\n').map((line, index) => ( +

{line}

+ ))} +
+ )} +
+
+
+ avatar image +

+ By{' '} + + {lesson.author?.username || lesson.author?.name || lesson.author?.pubkey} + +

+
+ +
+
+ { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + }} + /> +
+
+ {!isVideo && } + {lesson?.additionalLinks && lesson.additionalLinks.length > 0 && ( +
+

External links:

+ +
+ )} +
+ {!isVideo && renderContent()} +
+ ); +}; + +export default CombinedLesson; diff --git a/src/components/forms/course/LessonSelector.js b/src/components/forms/course/LessonSelector.js index 1a5969b..9caa518 100644 --- a/src/components/forms/course/LessonSelector.js +++ b/src/components/forms/course/LessonSelector.js @@ -67,6 +67,11 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe value: content })); + const combinedOptions = filteredContent.filter(content => content?.topics?.includes('video') && content?.topics?.includes('document') && content.kind).map(content => ({ + label: content.title, + value: content + })); + setContentOptions([ { label: 'Draft Documents', @@ -83,6 +88,10 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe { label: 'Published Videos', items: videoOptions + }, + { + label: 'Published Combined', + items: combinedOptions } ]); }; diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js index 2574711..bed68cd 100644 --- a/src/pages/course/[slug]/index.js +++ b/src/pages/course/[slug]/index.js @@ -4,6 +4,7 @@ import { parseCourseEvent, parseEvent, findKind0Fields } from "@/utils/nostr"; import CourseDetails from "@/components/content/courses/CourseDetails"; import VideoLesson from "@/components/content/courses/VideoLesson"; import DocumentLesson from "@/components/content/courses/DocumentLesson"; +import CombinedLesson from "@/components/content/courses/CombinedLesson"; import { useNDKContext } from "@/context/NDKContext"; import { useSession } from 'next-auth/react'; import axios from "axios"; @@ -225,6 +226,16 @@ const Course = () => { ); } + const renderLesson = (lesson) => { + if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) { + return ; + } else if (lesson.type === 'video' && !lesson.topics?.includes('document')) { + return ; + } else if (lesson.type === 'document' && !lesson.topics?.includes('video')) { + return ; + } + } + return ( <> {course && paidCourse !== null && ( @@ -260,10 +271,7 @@ const Course = () => { } >
- {lesson.type === 'video' ? - : - - } + {renderLesson(lesson)}
))}