import React, { useEffect, useState, useCallback } from "react"; import { useRouter } from "next/router"; 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"; import { nip04, nip19 } from "nostr-tools"; import { useToast } from "@/hooks/useToast"; import { ProgressSpinner } from "primereact/progressspinner"; import { Accordion, AccordionTab } from "primereact/accordion"; import { Tag } from "primereact/tag"; import { useDecryptContent } from "@/hooks/encryption/useDecryptContent"; import dynamic from "next/dynamic"; import ZapThreadsWrapper from "@/components/ZapThreadsWrapper"; import appConfig from "@/config/appConfig"; const MDDisplay = dynamic(() => import("@uiw/react-markdown-preview"), { ssr: false, }); const useCourseData = (ndk, fetchAuthor, router) => { const [course, setCourse] = useState(null); const [lessonIds, setLessonIds] = useState([]); const [paidCourse, setPaidCourse] = useState(null); const [loading, setLoading] = useState(true); const { showToast } = useToast(); useEffect(() => { if (!router.isReady) return; const { slug } = router.query; let id; const fetchCourseId = async () => { if (slug.includes("naddr")) { const { data } = nip19.decode(slug); if (!data?.identifier) { showToast("error", "Error", "Resource not found"); return null; } return data.identifier; } else { return slug; } }; const fetchCourse = async (courseId) => { try { await ndk.connect(); const event = await ndk.fetchEvent({ "#d": [courseId] }); if (!event) return null; const author = await fetchAuthor(event.pubkey); const lessonIds = event.tags .filter((tag) => tag[0] === "a") .map((tag) => tag[1].split(":")[2]); const parsedCourse = { ...parseCourseEvent(event), author }; return { parsedCourse, lessonIds }; } catch (error) { console.error("Error fetching event:", error); return null; } }; const initializeCourse = async () => { setLoading(true); id = await fetchCourseId(); if (!id) { setLoading(false); return; } const courseData = await fetchCourse(id); if (courseData) { const { parsedCourse, lessonIds } = courseData; setCourse(parsedCourse); setLessonIds(lessonIds); setPaidCourse(parsedCourse.price && parsedCourse.price > 0); } setLoading(false); }; initializeCourse(); }, [router.isReady, router.query, ndk, fetchAuthor, showToast]); return { course, lessonIds, paidCourse, loading }; }; const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { const [lessons, setLessons] = useState([]); const [uniqueLessons, setUniqueLessons] = useState([]); const { showToast } = useToast(); useEffect(() => { if (lessonIds.length > 0) { const fetchLesson = async (lessonId) => { try { await ndk.connect(); const filter = { "#d": [lessonId], kinds: [30023, 30402], authors: [pubkey], }; const event = await ndk.fetchEvent(filter); if (event) { const author = await fetchAuthor(event.pubkey); const parsedLesson = { ...parseEvent(event), author }; setLessons((prev) => { // Check if the lesson already exists in the array const exists = prev.some( (lesson) => lesson.id === parsedLesson.id ); if (!exists) { return [...prev, parsedLesson]; } return prev; }); } } catch (error) { console.error("Error fetching event:", error); } }; lessonIds.forEach((lessonId) => fetchLesson(lessonId)); } }, [lessonIds, ndk, fetchAuthor, pubkey]); useEffect(() => { const newUniqueLessons = Array.from( new Map(lessons.map((lesson) => [lesson.id, lesson])).values() ); setUniqueLessons(newUniqueLessons); }, [lessons]); return { lessons, uniqueLessons, setLessons }; }; const useDecryption = (session, paidCourse, course, lessons, setLessons) => { const [decryptionPerformed, setDecryptionPerformed] = useState(false); const [loading, setLoading] = useState(true); const { decryptContent } = useDecryptContent(); useEffect(() => { const decrypt = async () => { if (session?.user && paidCourse && !decryptionPerformed) { setLoading(true); const canAccess = session.user.purchased?.some( (purchase) => purchase.courseId === course?.d ) || session.user?.role?.subscribed || session.user?.pubkey === course?.pubkey; if (canAccess && lessons.length > 0) { try { const decryptedLessons = await Promise.all( lessons.map(async (lesson) => { const decryptedContent = await decryptContent(lesson.content); return { ...lesson, content: decryptedContent }; }) ); setLessons(decryptedLessons); setDecryptionPerformed(true); } catch (error) { console.error("Error decrypting lessons:", error); } } setLoading(false); } setLoading(false); }; decrypt(); }, [session, paidCourse, course, lessons, decryptionPerformed, setLessons]); return { decryptionPerformed, loading }; }; const Course = () => { const router = useRouter(); const { ndk, addSigner } = useNDKContext(); const { data: session, update } = useSession(); const { showToast } = useToast(); const [expandedIndex, setExpandedIndex] = useState(null); const [completedLessons, setCompletedLessons] = useState([]); const [nAddresses, setNAddresses] = useState({}); const [nsec, setNsec] = useState(null); const [npub, setNpub] = useState(null); const setCompleted = useCallback((lessonId) => { setCompletedLessons((prev) => [...prev, lessonId]); }, []); const fetchAuthor = useCallback( async (pubkey) => { const author = await ndk.getUser({ pubkey }); const profile = await author.fetchProfile(); const fields = await findKind0Fields(profile); return fields; }, [ndk] ); const { course, lessonIds, paidCourse, loading: courseLoading, } = useCourseData(ndk, fetchAuthor, router); const { lessons, uniqueLessons, setLessons } = useLessons( ndk, fetchAuthor, lessonIds, course?.pubkey ); const { decryptionPerformed, loading: decryptionLoading } = useDecryption( session, paidCourse, course, lessons, setLessons ); useEffect(() => { if (router.isReady) { const { active } = router.query; if (active !== undefined) { setExpandedIndex(parseInt(active, 10)); } else { setExpandedIndex(null); } } }, [router.isReady, router.query]); useEffect(() => { if (uniqueLessons.length > 0) { const addresses = {}; uniqueLessons.forEach((lesson) => { const addr = nip19.naddrEncode({ pubkey: lesson.pubkey, kind: lesson.kind, identifier: lesson.d, relays: appConfig.defaultRelayUrls, }); addresses[lesson.id] = addr; }); setNAddresses(addresses); } }, [uniqueLessons]); useEffect(() => { if (session?.user?.privkey) { const privkeyBuffer = Buffer.from(session.user.privkey, "hex"); setNsec(nip19.nsecEncode(privkeyBuffer)); } else if (session?.user?.pubkey) { setNpub(nip19.npubEncode(session.user.pubkey)); } }, [session]); const handleAccordionChange = (e) => { const newIndex = e.index === expandedIndex ? null : e.index; setExpandedIndex(newIndex); if (newIndex !== null) { router.push( `/course/${router.query.slug}?active=${newIndex}`, undefined, { shallow: true } ); } else { router.push(`/course/${router.query.slug}`, undefined, { shallow: true }); } }; const handlePaymentSuccess = async (response) => { if (response && response?.preimage) { const updated = await update(); showToast( "success", "Payment Success", "You have successfully purchased this course" ); } else { showToast( "error", "Error", "Failed to purchase course. Please try again." ); } }; const handlePaymentError = (error) => { showToast( "error", "Payment Error", `Failed to purchase course. Please try again. Error: ${error}` ); }; if (courseLoading || decryptionLoading) { return (
Comments are only available to course purchasers, subscribers, and the course creator.