2024-08-06 15:50:19 -05:00
|
|
|
import React, { useEffect, useState, useCallback } from "react";
|
2024-02-27 18:29:57 -06:00
|
|
|
import { useRouter } from "next/router";
|
2024-07-30 17:16:09 -05:00
|
|
|
import { parseCourseEvent, parseEvent, findKind0Fields } from "@/utils/nostr";
|
2024-09-14 16:43:03 -05:00
|
|
|
import CourseDetailsNew from "@/components/content/courses/CourseDetailsNew";
|
2024-09-12 17:39:47 -05:00
|
|
|
import VideoLesson from "@/components/content/courses/VideoLesson";
|
|
|
|
import DocumentLesson from "@/components/content/courses/DocumentLesson";
|
2024-08-06 15:50:19 -05:00
|
|
|
import { useNDKContext } from "@/context/NDKContext";
|
2024-08-16 18:00:46 -05:00
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
import { useSession } from 'next-auth/react';
|
2024-09-14 16:43:03 -05:00
|
|
|
import { nip04, nip19 } from 'nostr-tools';
|
2024-08-17 12:56:27 -05:00
|
|
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
2024-09-14 16:43:03 -05:00
|
|
|
import { Accordion, AccordionTab } from 'primereact/accordion';
|
|
|
|
import dynamic from 'next/dynamic';
|
2024-08-06 15:50:19 -05:00
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
const MDDisplay = dynamic(() => import("@uiw/react-markdown-preview"), { ssr: false });
|
2024-02-27 18:29:57 -06:00
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
const useCourseData = (ndk, fetchAuthor, router) => {
|
2024-02-27 18:29:57 -06:00
|
|
|
const [course, setCourse] = useState(null);
|
2024-07-30 17:16:09 -05:00
|
|
|
const [lessonIds, setLessonIds] = useState([]);
|
|
|
|
|
2024-02-27 18:29:57 -06:00
|
|
|
useEffect(() => {
|
2024-08-06 15:50:19 -05:00
|
|
|
if (router.isReady) {
|
|
|
|
const { slug } = router.query;
|
2024-09-14 16:43:03 -05:00
|
|
|
const { data } = nip19.decode(slug);
|
|
|
|
if (!data) {
|
|
|
|
showToast('error', 'Error', 'Course not found');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const id = data?.identifier;
|
|
|
|
const fetchCourse = async (id) => {
|
2024-08-06 15:50:19 -05:00
|
|
|
try {
|
|
|
|
await ndk.connect();
|
2024-09-14 16:43:03 -05:00
|
|
|
const filter = { ids: [id] };
|
2024-08-06 15:50:19 -05:00
|
|
|
const event = await ndk.fetchEvent(filter);
|
|
|
|
if (event) {
|
|
|
|
const author = await fetchAuthor(event.pubkey);
|
|
|
|
const aTags = event.tags.filter(tag => tag[0] === 'a');
|
|
|
|
const lessonIds = aTags.map(tag => tag[1].split(':')[2]);
|
|
|
|
setLessonIds(lessonIds);
|
2024-09-14 16:43:03 -05:00
|
|
|
const parsedCourse = { ...parseCourseEvent(event), author };
|
2024-08-06 15:50:19 -05:00
|
|
|
setCourse(parsedCourse);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error fetching event:', error);
|
2024-07-30 17:16:09 -05:00
|
|
|
}
|
2024-08-06 15:50:19 -05:00
|
|
|
};
|
2024-09-14 16:43:03 -05:00
|
|
|
if (ndk && id) {
|
|
|
|
fetchCourse(id);
|
2024-02-27 18:29:57 -06:00
|
|
|
}
|
|
|
|
}
|
2024-08-06 15:50:19 -05:00
|
|
|
}, [router.isReady, router.query, ndk, fetchAuthor]);
|
2024-02-27 18:29:57 -06:00
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
return { course, lessonIds };
|
|
|
|
};
|
|
|
|
|
2024-09-15 15:15:58 -05:00
|
|
|
const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
|
2024-09-14 16:43:03 -05:00
|
|
|
const [lessons, setLessons] = useState([]);
|
|
|
|
const [uniqueLessons, setUniqueLessons] = useState([]);
|
|
|
|
|
2024-09-15 15:15:58 -05:00
|
|
|
console.log('lessonIds', lessonIds);
|
|
|
|
|
2024-07-30 17:16:09 -05:00
|
|
|
useEffect(() => {
|
|
|
|
if (lessonIds.length > 0) {
|
|
|
|
const fetchLesson = async (lessonId) => {
|
2024-09-15 15:15:58 -05:00
|
|
|
console.log('lessonId', lessonId);
|
2024-07-30 17:16:09 -05:00
|
|
|
try {
|
2024-08-06 15:50:19 -05:00
|
|
|
await ndk.connect();
|
2024-09-15 15:15:58 -05:00
|
|
|
const filter = { "#d": [lessonId], kinds:[30023, 30402], authors: [pubkey] };
|
2024-08-06 15:50:19 -05:00
|
|
|
const event = await ndk.fetchEvent(filter);
|
|
|
|
if (event) {
|
|
|
|
const author = await fetchAuthor(event.pubkey);
|
2024-09-14 16:43:03 -05:00
|
|
|
const parsedLesson = { ...parseEvent(event), author };
|
2024-08-06 15:50:19 -05:00
|
|
|
setLessons(prev => [...prev, parsedLesson]);
|
2024-07-30 17:16:09 -05:00
|
|
|
}
|
|
|
|
} catch (error) {
|
2024-08-06 15:50:19 -05:00
|
|
|
console.error('Error fetching event:', error);
|
2024-07-30 17:16:09 -05:00
|
|
|
}
|
2024-08-06 15:50:19 -05:00
|
|
|
};
|
2024-07-30 17:16:09 -05:00
|
|
|
lessonIds.forEach(lessonId => fetchLesson(lessonId));
|
|
|
|
}
|
2024-08-06 15:50:19 -05:00
|
|
|
}, [lessonIds, ndk, fetchAuthor]);
|
2024-07-30 17:16:09 -05:00
|
|
|
|
2024-08-25 12:12:55 -05:00
|
|
|
useEffect(() => {
|
2024-09-14 16:43:03 -05:00
|
|
|
const uniqueLessonSet = new Set(lessons.map(JSON.stringify));
|
|
|
|
const newUniqueLessons = Array.from(uniqueLessonSet).map(JSON.parse);
|
|
|
|
setUniqueLessons(newUniqueLessons);
|
2024-08-25 12:12:55 -05:00
|
|
|
}, [lessons]);
|
|
|
|
|
2024-09-15 15:15:58 -05:00
|
|
|
useEffect(() => {
|
|
|
|
console.log('uniqueLessons', uniqueLessons);
|
|
|
|
}, [uniqueLessons]);
|
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
return { lessons, uniqueLessons, setLessons };
|
|
|
|
};
|
2024-08-25 12:12:55 -05:00
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
const useDecryption = (session, paidCourse, course, lessons, setLessons) => {
|
|
|
|
const [decryptionPerformed, setDecryptionPerformed] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY;
|
|
|
|
const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY;
|
2024-08-16 18:00:46 -05:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const decryptContent = async () => {
|
2024-08-17 12:56:27 -05:00
|
|
|
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 nip04.decrypt(privkey, pubkey, lesson.content);
|
|
|
|
return { ...lesson, content: decryptedContent };
|
|
|
|
}));
|
|
|
|
setLessons(decryptedLessons);
|
|
|
|
setDecryptionPerformed(true);
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error decrypting lessons:', error);
|
2024-08-16 18:00:46 -05:00
|
|
|
}
|
|
|
|
}
|
2024-08-17 12:56:27 -05:00
|
|
|
setLoading(false);
|
2024-08-16 18:00:46 -05:00
|
|
|
}
|
2024-08-17 12:56:27 -05:00
|
|
|
setLoading(false);
|
2024-08-16 18:00:46 -05:00
|
|
|
}
|
|
|
|
decryptContent();
|
2024-09-14 16:43:03 -05:00
|
|
|
}, [session, paidCourse, course, lessons, privkey, pubkey, decryptionPerformed, setLessons]);
|
|
|
|
|
|
|
|
return { decryptionPerformed, loading };
|
|
|
|
};
|
|
|
|
|
|
|
|
const Course = () => {
|
|
|
|
const router = useRouter();
|
|
|
|
const { ndk, addSigner } = useNDKContext();
|
|
|
|
const { data: session, update } = useSession();
|
|
|
|
const { showToast } = useToast();
|
|
|
|
const [paidCourse, setPaidCourse] = useState(false);
|
|
|
|
const [expandedIndex, setExpandedIndex] = useState(null);
|
|
|
|
|
|
|
|
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 } = useCourseData(ndk, fetchAuthor, router);
|
2024-09-15 15:15:58 -05:00
|
|
|
const { lessons, uniqueLessons, setLessons } = useLessons(ndk, fetchAuthor, lessonIds, course?.pubkey);
|
2024-09-14 16:43:03 -05:00
|
|
|
const { decryptionPerformed, loading } = useDecryption(session, paidCourse, course, lessons, setLessons);
|
2024-08-17 12:56:27 -05:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-09-14 16:43:03 -05:00
|
|
|
if (course?.price && course?.price > 0) {
|
|
|
|
setPaidCourse(true);
|
|
|
|
}
|
|
|
|
}, [course]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (router.isReady) {
|
|
|
|
const { active } = router.query;
|
|
|
|
if (active !== undefined) {
|
|
|
|
setExpandedIndex(parseInt(active, 10));
|
|
|
|
} else {
|
|
|
|
setExpandedIndex(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [router.isReady, router.query]);
|
|
|
|
|
|
|
|
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 });
|
2024-08-17 12:56:27 -05:00
|
|
|
}
|
2024-09-14 16:43:03 -05:00
|
|
|
};
|
2024-08-16 18:00:46 -05:00
|
|
|
|
2024-09-12 09:17:22 -05:00
|
|
|
const handlePaymentSuccess = async (response) => {
|
2024-08-16 18:00:46 -05:00
|
|
|
if (response && response?.preimage) {
|
|
|
|
const updated = await update();
|
|
|
|
console.log("session after update", updated);
|
2024-09-12 09:17:22 -05:00
|
|
|
showToast('success', 'Payment Success', 'You have successfully purchased this course');
|
|
|
|
router.reload();
|
2024-08-16 18:00:46 -05:00
|
|
|
} 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}`);
|
|
|
|
}
|
|
|
|
|
2024-08-17 12:56:27 -05:00
|
|
|
if (loading) {
|
|
|
|
return (
|
2024-09-17 13:28:58 -05:00
|
|
|
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
|
2024-08-17 12:56:27 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-02-27 18:29:57 -06:00
|
|
|
return (
|
2024-07-30 17:16:09 -05:00
|
|
|
<>
|
2024-09-12 17:39:47 -05:00
|
|
|
<CourseDetailsNew
|
2024-08-16 18:00:46 -05:00
|
|
|
processedEvent={course}
|
|
|
|
paidCourse={paidCourse}
|
2024-09-14 16:43:03 -05:00
|
|
|
lessons={uniqueLessons}
|
2024-08-17 12:56:27 -05:00
|
|
|
decryptionPerformed={decryptionPerformed}
|
2024-08-16 18:00:46 -05:00
|
|
|
handlePaymentSuccess={handlePaymentSuccess}
|
|
|
|
handlePaymentError={handlePaymentError}
|
|
|
|
/>
|
2024-09-14 16:43:03 -05:00
|
|
|
<Accordion
|
|
|
|
activeIndex={expandedIndex}
|
|
|
|
onTabChange={handleAccordionChange}
|
2024-09-16 16:10:28 -05:00
|
|
|
className="mt-4 px-4 max-mob:px-0 max-tab:px-0"
|
2024-09-14 16:43:03 -05:00
|
|
|
>
|
|
|
|
{uniqueLessons.length > 0 && uniqueLessons.map((lesson, index) => (
|
|
|
|
<AccordionTab
|
|
|
|
key={index}
|
|
|
|
pt={{
|
|
|
|
root: { className: 'border-none' },
|
|
|
|
header: { className: 'border-none' },
|
|
|
|
headerAction: { className: 'border-none' },
|
2024-09-16 16:10:28 -05:00
|
|
|
content: { className: 'border-none max-mob:px-0 max-tab:px-0' },
|
2024-09-14 16:43:03 -05:00
|
|
|
accordiontab: { className: 'border-none' },
|
|
|
|
}}
|
|
|
|
header={
|
|
|
|
<div className="flex align-items-center justify-content-between w-full">
|
|
|
|
<span id={`lesson-${index}`} className="font-bold text-xl">{`Lesson ${index + 1}: ${lesson.title}`}</span>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<div className="w-full py-4 rounded-b-lg">
|
2024-09-15 13:27:37 -05:00
|
|
|
{lesson.type === 'video' ?
|
2024-09-14 16:43:03 -05:00
|
|
|
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} /> :
|
|
|
|
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} />
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
</AccordionTab>
|
|
|
|
))}
|
|
|
|
</Accordion>
|
2024-02-27 18:29:57 -06:00
|
|
|
<div className="mx-auto my-6">
|
2024-09-03 17:02:24 -05:00
|
|
|
{course?.content && <MDDisplay className='p-4 rounded-lg' source={course.content} />}
|
2024-02-27 18:29:57 -06:00
|
|
|
</div>
|
2024-07-30 17:16:09 -05:00
|
|
|
</>
|
2024-02-27 18:29:57 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Course;
|