From 3efc82bd0643209a50c264fd35265bc9ee409970 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Sun, 30 Mar 2025 11:46:56 -0500 Subject: [PATCH] more options button in place and works, fully tested, probably still need to align with timestamp --- .../content/courses/CombinedLesson.js | 46 ++++++++++++- .../content/courses/CourseLesson.js | 69 ++++++++++++++++++- src/components/content/courses/VideoLesson.js | 46 ++++++++++++- src/config/appConfig.js | 2 +- src/hooks/tracking/useTrackDocumentLesson.js | 2 +- src/hooks/tracking/useTrackVideoLesson.js | 2 +- 6 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/components/content/courses/CombinedLesson.js b/src/components/content/courses/CombinedLesson.js index 3428395..0175a35 100644 --- a/src/components/content/courses/CombinedLesson.js +++ b/src/components/content/courses/CombinedLesson.js @@ -12,6 +12,8 @@ import dynamic from "next/dynamic"; import useWindowWidth from "@/hooks/useWindowWidth"; import appConfig from "@/config/appConfig"; import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson'; +import { Menu } from "primereact/menu"; +import { Toast } from "primereact/toast"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -26,13 +28,15 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple const [videoDuration, setVideoDuration] = useState(null); const [videoPlayed, setVideoPlayed] = useState(false); const mdDisplayRef = useRef(null); + const menuRef = useRef(null); + const toastRef = 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({ + const { isCompleted: videoCompleted, isTracking: videoTracking, markLessonAsCompleted } = useTrackVideoLesson({ lessonId: lesson?.d, videoDuration, courseId: course?.d, @@ -41,6 +45,33 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple decryptionPerformed }); + const menuItems = [ + { + label: 'Mark as completed', + icon: 'pi pi-check-circle', + command: async () => { + try { + await markLessonAsCompleted(); + setCompleted(lesson.id); + toastRef.current.show({ + severity: 'success', + summary: 'Success', + detail: 'Lesson marked as completed', + life: 3000 + }); + } catch (error) { + console.error('Failed to mark lesson as completed:', error); + toastRef.current.show({ + severity: 'error', + summary: 'Error', + detail: 'Failed to mark lesson as completed', + life: 3000 + }); + } + } + } + ]; + useEffect(() => { const handleYouTubeMessage = (event) => { if (event.origin !== "https://www.youtube.com") return; @@ -168,6 +199,7 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple return (
+ {isVideo ? renderContent() : ( <>
@@ -252,6 +284,18 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple )}
{!isVideo && renderContent()} + +
+ + menuRef.current.toggle(e)} + aria-label="More options" + className="p-button-text" + tooltip={isMobileView ? null : "More options"} + tooltipOptions={{ position: 'top' }} + /> +
); }; diff --git a/src/components/content/courses/CourseLesson.js b/src/components/content/courses/CourseLesson.js index 2b04dd8..8e00cf7 100644 --- a/src/components/content/courses/CourseLesson.js +++ b/src/components/content/courses/CourseLesson.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { Tag } from "primereact/tag"; import Image from "next/image"; import { useImageProxy } from "@/hooks/useImageProxy"; @@ -6,6 +6,11 @@ import { getTotalFromZaps } from "@/utils/lightning"; import ZapDisplay from "@/components/zaps/ZapDisplay"; import dynamic from "next/dynamic"; import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; +import { Menu } from "primereact/menu"; +import { Toast } from "primereact/toast"; +import GenericButton from "@/components/buttons/GenericButton"; +import useTrackDocumentLesson from "@/hooks/tracking/useTrackDocumentLesson"; +import useWindowWidth from "@/hooks/useWindowWidth"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -14,10 +19,51 @@ const MDDisplay = dynamic( } ); -const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => { +const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => { const [zapAmount, setZapAmount] = useState(0); const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" }); const { returnImageProxy } = useImageProxy(); + const menuRef = useRef(null); + const toastRef = useRef(null); + const windowWidth = useWindowWidth(); + const isMobileView = windowWidth <= 768; + + const readTime = lesson?.content ? Math.max(30, Math.ceil(lesson.content.length / 20)) : 60; + + const { isCompleted, isTracking, markLessonAsCompleted } = useTrackDocumentLesson({ + lessonId: lesson?.d, + courseId: course?.d, + readTime, + paidCourse: isPaid, + decryptionPerformed + }); + + const menuItems = [ + { + label: 'Mark as completed', + icon: 'pi pi-check-circle', + command: async () => { + try { + await markLessonAsCompleted(); + setCompleted && setCompleted(lesson.id); + toastRef.current.show({ + severity: 'success', + summary: 'Success', + detail: 'Lesson marked as completed', + life: 3000 + }); + } catch (error) { + console.error('Failed to mark lesson as completed:', error); + toastRef.current.show({ + severity: 'error', + summary: 'Error', + detail: 'Failed to mark lesson as completed', + life: 3000 + }); + } + } + } + ]; useEffect(() => { if (!zaps || zapsLoading || zapsError) return; @@ -26,6 +72,12 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => { setZapAmount(total); }, [zaps, zapsLoading, zapsError, lesson]); + + useEffect(() => { + if (isCompleted && !isTracking && setCompleted) { + setCompleted(lesson.id); + } + }, [isCompleted, isTracking, lesson.id, setCompleted]); const renderContent = () => { if (isPaid && decryptionPerformed) { @@ -42,6 +94,7 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => { return (
+
@@ -112,6 +165,18 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
{renderContent()}
+ +
+ + menuRef.current.toggle(e)} + aria-label="More options" + className="p-button-text" + tooltip={isMobileView ? null : "More options"} + tooltipOptions={{ position: 'top' }} + /> +
) } diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js index d2b9fb8..98b4b04 100644 --- a/src/components/content/courses/VideoLesson.js +++ b/src/components/content/courses/VideoLesson.js @@ -12,6 +12,8 @@ import { Divider } from "primereact/divider"; import appConfig from "@/config/appConfig"; import useWindowWidth from "@/hooks/useWindowWidth"; import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson'; +import { Menu } from "primereact/menu"; +import { Toast } from "primereact/toast"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -30,8 +32,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted const [videoDuration, setVideoDuration] = useState(null); const [videoPlayed, setVideoPlayed] = useState(false); const mdDisplayRef = useRef(null); + const menuRef = useRef(null); + const toastRef = useRef(null); - const { isCompleted, isTracking } = useTrackVideoLesson({ + const { isCompleted, isTracking, markLessonAsCompleted } = useTrackVideoLesson({ lessonId: lesson?.d, videoDuration, courseId: course?.d, @@ -39,6 +43,33 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted paidCourse: isPaid, decryptionPerformed }); + + const menuItems = [ + { + label: 'Mark as completed', + icon: 'pi pi-check-circle', + command: async () => { + try { + await markLessonAsCompleted(); + setCompleted(lesson.id); + toastRef.current.show({ + severity: 'success', + summary: 'Success', + detail: 'Lesson marked as completed', + life: 3000 + }); + } catch (error) { + console.error('Failed to mark lesson as completed:', error); + toastRef.current.show({ + severity: 'error', + summary: 'Error', + detail: 'Failed to mark lesson as completed', + life: 3000 + }); + } + } + } + ]; useEffect(() => { const handleYouTubeMessage = (event) => { @@ -148,6 +179,7 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted return (
+ {renderContent()}
@@ -219,6 +251,18 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
)}
+ +
+ + menuRef.current.toggle(e)} + aria-label="More options" + className="p-button-text" + tooltip={isMobileView ? null : "More options"} + tooltipOptions={{ position: 'top' }} + /> +
) } diff --git a/src/config/appConfig.js b/src/config/appConfig.js index 784cb30..98cefb8 100644 --- a/src/config/appConfig.js +++ b/src/config/appConfig.js @@ -9,7 +9,7 @@ const appConfig = { "wss://purplerelay.com/", "wss://relay.devs.tools/" ], - authorPubkeys: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345"], + authorPubkeys: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741", "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345", "6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4"], customLightningAddresses: [ { // todo remove need for lowercase diff --git a/src/hooks/tracking/useTrackDocumentLesson.js b/src/hooks/tracking/useTrackDocumentLesson.js index c61a712..35bc9e7 100644 --- a/src/hooks/tracking/useTrackDocumentLesson.js +++ b/src/hooks/tracking/useTrackDocumentLesson.js @@ -105,7 +105,7 @@ const useTrackDocumentLesson = ({ lessonId, courseId, readTime, paidCourse, decr } }, [timeSpent, markLessonAsCompleted, readTime, isAdmin]); - return { isCompleted, isTracking }; + return { isCompleted, isTracking, markLessonAsCompleted }; }; export default useTrackDocumentLesson; \ No newline at end of file diff --git a/src/hooks/tracking/useTrackVideoLesson.js b/src/hooks/tracking/useTrackVideoLesson.js index f322977..e778fbf 100644 --- a/src/hooks/tracking/useTrackVideoLesson.js +++ b/src/hooks/tracking/useTrackVideoLesson.js @@ -134,7 +134,7 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed, pa } }, [timeSpent, videoDuration, markLessonAsCompleted, isAdmin]); - return { isCompleted, isTracking }; + return { isCompleted, isTracking, markLessonAsCompleted }; }; export default useTrackVideoLesson; \ No newline at end of file