diff --git a/src/components/content/carousels/WorkshopsCarousel.js b/src/components/content/carousels/WorkshopsCarousel.js index 94c1a13..f907475 100644 --- a/src/components/content/carousels/WorkshopsCarousel.js +++ b/src/components/content/carousels/WorkshopsCarousel.js @@ -31,7 +31,9 @@ export default function WorkshopsCarousel() { const fetch = async () => { try { if (workshops && workshops.length > 0) { + console.log('workshops', workshops); const processedWorkshops = workshops.map(workshop => parseEvent(workshop)); + console.log('processedWorkshops', processedWorkshops); setProcessedWorkshops(processedWorkshops); } else { console.log('No workshops fetched or empty array returned'); diff --git a/src/components/content/courses/CourseDetails.js b/src/components/content/courses/CourseDetails.js index b5f3634..669e25c 100644 --- a/src/components/content/courses/CourseDetails.js +++ b/src/components/content/courses/CourseDetails.js @@ -13,7 +13,8 @@ import { useNDKContext } from "@/context/NDKContext"; import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription'; import { findKind0Fields } from '@/utils/nostr'; import 'primeicons/primeicons.css'; -import ResourcePaymentButton from "@/components/bitcoinConnect/ResourcePaymentButton"; +import CoursePaymentButton from "@/components/bitcoinConnect/CoursePaymentButton"; +import { ProgressSpinner } from 'primereact/progressspinner'; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -22,7 +23,7 @@ const MDDisplay = dynamic( } ); -export default function CourseDetails({ processedEvent, paidCourse, decryptedContent, handlePaymentSuccess, handlePaymentError }) { +export default function CourseDetails({ processedEvent, paidCourse, lessons, decryptionPerformed, handlePaymentSuccess, handlePaymentError }) { const [author, setAuthor] = useState(null); const [nAddress, setNAddress] = useState(null); const [zapAmount, setZapAmount] = useState(0); @@ -45,6 +46,10 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon console.log("paidCourse", paidCourse); }, [paidCourse]); + useEffect(() => { + console.log("decryptionPerformed", decryptionPerformed); + }, [decryptionPerformed]); + useEffect(() => { if (session) { setUser(session.user); @@ -86,6 +91,14 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon setZapAmount(total); }, [zaps, processedEvent]); + if (!processedEvent || !author) { + return ( +
+ +
+ ); + } + return (
@@ -128,8 +141,8 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon className="w-[344px] h-[194px] object-cover object-top rounded-lg" />
- {paidCourse && !decryptedContent && ( - )} - {paidCourse && decryptedContent && author && processedEvent?.pubkey !== session?.user?.pubkey && ( + {paidCourse && decryptionPerformed && author && processedEvent?.pubkey !== session?.user?.pubkey && (

Paid {processedEvent.price} sats

)} {paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey && ( diff --git a/src/components/content/lists/ContentListItem.js b/src/components/content/lists/ContentListItem.js index 404c3bb..0f6cd33 100644 --- a/src/components/content/lists/ContentListItem.js +++ b/src/components/content/lists/ContentListItem.js @@ -10,14 +10,6 @@ const ContentListItem = (content) => { const isDraft = Object.keys(content).includes('type'); const isCourse = content && content?.kind === 30004; - useEffect(() => { - if (content && content?.kind === 30004) { - console.log("isDraft", isDraft); - console.log("content", content); - console.log("isCourse", isCourse); - } - }, [content, isDraft, isCourse]); - const handleClick = () => { let path = ''; diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 991909a..e4c1865 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -26,8 +26,8 @@ import 'primeicons/primeicons.css'; const CourseForm = ({ draft = null, isPublished = false }) => { const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); - const [checked, setChecked] = useState(false); - const [price, setPrice] = useState(0); + const [isPaidCourse, setIsPaidCourse] = useState(draft?.price ? true : false); + const [price, setPrice] = useState(draft?.price || 0); const [coverImage, setCoverImage] = useState(''); const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]); const [loadingLessons, setLoadingLessons] = useState(true); @@ -89,7 +89,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => { console.log('draft:', draft); setTitle(draft.title); setSummary(draft.summary); - setChecked(draft.price > 0); + setIsPaidCourse(draft.price > 0); setPrice(draft.price || 0); setCoverImage(draft.image); // setSelectedLessons(draft.resources || []); @@ -97,6 +97,10 @@ const CourseForm = ({ draft = null, isPublished = false }) => { } }, [draft]); + const handlePriceChange = (value) => { + setPrice(value); + }; + const handleSubmit = async (e) => { e.preventDefault(); @@ -198,12 +202,20 @@ const CourseForm = ({ draft = null, isPublished = false }) => { if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) { return []; } - const draftOptions = drafts.map(draft => ({ + + const filterContent = (content) => { + console.log('contentttttt', content); + // If there is price in content.tags, then it is a paid content 'price' in the 0 index and stringified int in the 1 index + const contentPrice = content.tags.find(tag => tag[0] === 'price') ? parseInt(content.tags.find(tag => tag[0] === 'price')[1]) : 0; + return isPaidCourse ? contentPrice > 0 : contentPrice === 0; + }; + + const draftOptions = drafts.filter(filterContent).map(draft => ({ label: handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />, value: draft.id })); - const resourceOptions = resources.map(resource => { + const resourceOptions = resources.filter(filterContent).map(resource => { const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resource); return { label: handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />, @@ -211,7 +223,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => { }; }); - const workshopOptions = workshops.map(workshop => { + const workshopOptions = workshops.filter(filterContent).map(workshop => { const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(workshop); return { label: handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />, @@ -251,12 +263,17 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
setCoverImage(e.target.value)} placeholder="Cover Image URL" />
-
+

Paid Course

- setChecked(e.value)} /> - {checked && ( + setIsPaidCourse(e.value)} /> + {isPaidCourse && (
- setPrice(e.value)} placeholder="Price (sats)" /> + handlePriceChange(e.value)} + placeholder="Price (sats)" + min={1} + />
)}
diff --git a/src/components/forms/EditCourseForm.js b/src/components/forms/EditCourseForm.js index f0a2437..5956298 100644 --- a/src/components/forms/EditCourseForm.js +++ b/src/components/forms/EditCourseForm.js @@ -25,6 +25,7 @@ const EditCourseForm = ({ draft }) => { const [summary, setSummary] = useState(''); const [checked, setChecked] = useState(false); const [price, setPrice] = useState(0); + const [isPaid, setIsPaid] = useState(false); const [coverImage, setCoverImage] = useState(''); const [selectedLessons, setSelectedLessons] = useState([]); const [selectedLessonsLoading, setSelectedLessonsLoading] = useState(false); @@ -63,11 +64,17 @@ const EditCourseForm = ({ draft }) => { setSummary(draft.summary); setChecked(draft.price > 0); setPrice(draft.price || 0); + setIsPaid(draft.price > 0); setCoverImage(draft.image); setTopics(draft.topics || ['']); } }, [draft, ndk, showToast, parseEvent]); + const handlePriceChange = (value) => { + setPrice(value); + setIsPaid(value > 0); + }; + const handleSubmit = async (e) => { e.preventDefault(); @@ -90,7 +97,7 @@ const EditCourseForm = ({ draft }) => { title, summary, image: coverImage, - price: checked ? price : 0, + price: isPaid ? price : 0, topics, resourceIds: lessonsToUpdate.filter(lesson => lesson && lesson.id).map(lesson => lesson.id) }; @@ -137,7 +144,12 @@ const EditCourseForm = ({ draft }) => { return []; } - const resourceOptions = resources.map(resource => { + const filterContent = (content) => { + const contentPrice = content.price || 0; + return isPaid ? contentPrice > 0 : contentPrice === 0; + }; + + const resourceOptions = resources.filter(filterContent).map(resource => { const parsedResource = parseEvent(resource); return { label: lesson.id === parsedResource.id)} />, @@ -145,7 +157,7 @@ const EditCourseForm = ({ draft }) => { }; }); - const workshopOptions = workshops.map(workshop => { + const workshopOptions = workshops.filter(filterContent).map(workshop => { const parsedWorkshop = parseEvent(workshop); return { label: lesson.id === parsedWorkshop.id)} />, @@ -179,7 +191,7 @@ const EditCourseForm = ({ draft }) => { setChecked(e.value)} /> {checked && (
- setPrice(e.value)} placeholder="Price (sats)" /> + handlePriceChange(e.value)} placeholder="Price (sats)" />
)}
diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js index a26b1fa..da07c12 100644 --- a/src/pages/course/[slug]/index.js +++ b/src/pages/course/[slug]/index.js @@ -8,6 +8,7 @@ import { useNDKContext } from "@/context/NDKContext"; import { useToast } from '@/hooks/useToast'; import { useSession } from 'next-auth/react'; import { nip04 } from 'nostr-tools'; +import { ProgressSpinner } from 'primereact/progressspinner'; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -21,12 +22,13 @@ const Course = () => { const [lessonIds, setLessonIds] = useState([]); const [lessons, setLessons] = useState([]); const [paidCourse, setPaidCourse] = useState(false); - const [decryptedContent, setDecryptedContent] = useState(null); - + const [decryptionPerformed, setDecryptionPerformed] = useState(false); + const [loading, setLoading] = useState(true); const router = useRouter(); const {ndk, addSigner} = useNDKContext(); const { data: session, update } = useSession(); const { showToast } = useToast(); + const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY; const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY; @@ -112,21 +114,37 @@ const Course = () => { useEffect(() => { const decryptContent = async () => { - if (session?.user && paidCourse) { - if (session.user?.purchased?.length > 0) { - const purchasedCourse = session.user.purchased.find(purchase => purchase.resourceId === course.d); - if (purchasedCourse) { - const decryptedContent = await nip04.decrypt(privkey, pubkey, course.content); - setDecryptedContent(decryptedContent); + 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); } - } else if (session.user?.role && session.user.role.subscribed) { - const decryptedContent = await nip04.decrypt(privkey, pubkey, course.content); - setDecryptedContent(decryptedContent); } + setLoading(false); } + setLoading(false); } decryptContent(); - }, [session, paidCourse, course]); + }, [session, paidCourse, course, lessons, privkey, pubkey, decryptionPerformed]); + + useEffect(() => { + if (course && lessons.length > 0 && (!paidCourse || decryptionPerformed)) { + setLoading(false); + } + }, [course, lessons, paidCourse, decryptionPerformed]); const handlePaymentSuccess = async (response, newCourse) => { if (response && response?.preimage) { @@ -142,12 +160,21 @@ const Course = () => { showToast('error', 'Payment Error', `Failed to purchase course. Please try again. Error: ${error}`); } + if (loading) { + return ( +
+ +
+ ); + } + return ( <> @@ -155,9 +182,7 @@ const Course = () => { ))}
- { - course?.content && - } + {course?.content && }
); diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index 2bae292..34996f9 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -227,26 +227,11 @@ export default function Draft() { ['image', draft.image], ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], + ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), ]; type = 'workshop'; break; - case 'course': - event.kind = 30023; - event.content = draft.content; - event.created_at = Math.floor(Date.now() / 1000); - event.pubkey = user.pubkey; - event.tags = [ - ['d', NewDTag], - ['title', draft.title], - ['summary', draft.summary], - ['image', draft.image], - ...draft.topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()], - ]; - - type = 'course'; - break; default: return null; }