diff --git a/src/components/CourseDetails.js b/src/components/CourseDetails.js new file mode 100644 index 0000000..0173e79 --- /dev/null +++ b/src/components/CourseDetails.js @@ -0,0 +1,170 @@ +"use client"; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import { useNostr } from '@/hooks/useNostr'; +import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr'; +import { useImageProxy } from '@/hooks/useImageProxy'; +import { Button } from 'primereact/button'; +import { Tag } from 'primereact/tag'; +import { nip19 } from 'nostr-tools'; +import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import Image from 'next/image'; +import dynamic from 'next/dynamic'; +import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; +import 'primeicons/primeicons.css'; +const MDDisplay = dynamic( + () => import("@uiw/react-markdown-preview"), + { + ssr: false, + } +); + +const BitcoinConnectPayButton = dynamic( + () => import('@getalby/bitcoin-connect-react').then((mod) => mod.PayButton), + { + ssr: false, + } +); + +export default function CourseDetails({processedEvent}) { + const [author, setAuthor] = useState(null); + const [bitcoinConnect, setBitcoinConnect] = useState(false); + const [nAddress, setNAddress] = useState(null); + const [user] = useLocalStorageWithEffect('user', {}); + console.log('user:', user); + const { returnImageProxy } = useImageProxy(); + const { fetchSingleEvent, fetchKind0, zapEvent } = useNostr(); + + const router = useRouter(); + + const handleZapEvent = async () => { + if (!processedEvent) return; + + const response = await zapEvent(processedEvent); + + console.log('zap response:', response); + } + + useEffect(() => { + if (typeof window === 'undefined') return; + + const bitcoinConnectConfig = window.localStorage.getItem('bc:config'); + + if (bitcoinConnectConfig) { + setBitcoinConnect(true); + } + }, []); + + useEffect(() => { + const fetchAuthor = async (pubkey) => { + const author = await fetchKind0(pubkey); + const fields = await findKind0Fields(author); + console.log('fields:', fields); + if (fields) { + setAuthor(fields); + } + } + if (processedEvent) { + fetchAuthor(processedEvent.pubkey); + } + }, [fetchKind0, processedEvent]); + + useEffect(() => { + if (processedEvent?.d) { + const naddr = nip19.naddrEncode({ + pubkey: processedEvent.pubkey, + kind: processedEvent.kind, + identifier: processedEvent.d, + }); + console.log('naddr:', naddr); + setNAddress(naddr); + } + }, [processedEvent]); + + return ( +
+
+ router.push('/')} /> +
+
+
+ {processedEvent && processedEvent.topics && processedEvent.topics.length > 0 && ( + processedEvent.topics.map((topic, index) => ( + + )) + ) + } +
+

{processedEvent?.title}

+

{processedEvent?.summary}

+
+ avatar thumbnail +

+ Created by{' '} + + {author?.username} + +

+
+
+
+ {processedEvent && ( +
+ resource thumbnail + {bitcoinConnect ? ( +
+ +
+ ) : ( +
+
+ )} +
+ )} +
+
+
+ {typeof window !== 'undefined' && nAddress !== null && ( +
+ +
+ )} +
+ { + processedEvent?.content && + } +
+
+ ); +} \ No newline at end of file diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js index 2294c2d..be454f6 100644 --- a/src/components/content/carousels/templates/CourseTemplate.js +++ b/src/components/content/carousels/templates/CourseTemplate.js @@ -32,7 +32,7 @@ const CourseTemplate = (course) => { className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md" >
router.push(`/details/${course.id}`)} + onClick={() => router.push(`/course/${course.id}`)} className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer" style={{ paddingBottom: "56.25%" }} > diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index 4fca34a..ed9a336 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -97,6 +97,32 @@ export function useNostr() { [subscribe] ); + const fetchSingleNaddrEvent = useCallback( + async (id) => { + try { + const event = await new Promise((resolve, reject) => { + subscribe( + [{ "#d": [id] }], + { + onevent: (event) => { + resolve(event); + }, + onerror: (error) => { + console.error('Failed to fetch event:', error); + reject(error); + }, + } + ); + }); + return event; + } catch (error) { + console.error('Failed to fetch event:', error); + return null; + } + }, + [subscribe] + ); + const querySyncQueue = useRef([]); const lastQuerySyncTime = useRef(0); @@ -535,5 +561,5 @@ export function useNostr() { [publish] ); - return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; + return { subscribe, publish, fetchSingleEvent, fetchSingleNaddrEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; } \ No newline at end of file diff --git a/src/pages/course/[slug].js b/src/pages/course/[slug].js index 0b95997..25bdeac 100644 --- a/src/pages/course/[slug].js +++ b/src/pages/course/[slug].js @@ -1,7 +1,13 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { useNostr } from "@/hooks/useNostr"; -import { parseCourseEvent } from "@/utils/nostr"; +import { parseCourseEvent, parseEvent, findKind0Fields } from "@/utils/nostr"; +import { useImageProxy } from "@/hooks/useImageProxy"; +import { Button } from "primereact/button"; +import { Tag } from "primereact/tag"; +import Image from "next/image"; +import CourseDetails from "@/components/CourseDetails"; +import { nip19 } from "nostr-tools"; import dynamic from 'next/dynamic'; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -9,21 +15,63 @@ const MDDisplay = dynamic( ssr: false, } ); +const BitcoinConnectPayButton = dynamic( + () => import('@getalby/bitcoin-connect-react').then((mod) => mod.PayButton), + { + ssr: false, + } +); const Course = () => { const [course, setCourse] = useState(null); + const [lessonIds, setLessonIds] = useState([]); + const [lessons, setLessons] = useState([]); + const [bitcoinConnect, setBitcoinConnect] = useState(false); const router = useRouter(); - const { fetchSingleEvent } = useNostr(); + const { fetchSingleEvent, fetchSingleNaddrEvent, fetchKind0 } = useNostr(); + const { returnImageProxy } = useImageProxy(); const { slug } = router.query; + const fetchAuthor = async (pubkey) => { + const author = await fetchKind0(pubkey); + const fields = await findKind0Fields(author); + if (fields) { + return fields; + } + } + + const handleZapEvent = async () => { + if (!event) return; + + const response = await zapEvent(event); + + console.log('zap response:', response); + } + + useEffect(() => { + if (typeof window === 'undefined') return; + + const bitcoinConnectConfig = window.localStorage.getItem('bc:config'); + + if (bitcoinConnectConfig) { + setBitcoinConnect(true); + } + }, []); + useEffect(() => { const getCourse = async () => { if (slug) { const fetchedCourse = await fetchSingleEvent(slug); const formattedCourse = parseCourseEvent(fetchedCourse); + const aTags = formattedCourse.tags.filter(tag => tag[0] === 'a'); setCourse(formattedCourse); + if (aTags.length > 0) { + const lessonIds = aTags.map(tag => tag[1]); + setLessonIds(lessonIds); + console.log("LESSON IDS", lessonIds); + } } }; @@ -32,16 +80,119 @@ const Course = () => { } }, [slug]); + useEffect(() => { + if (lessonIds.length > 0) { + + const fetchLesson = async (lessonId) => { + try { + const l = await fetchSingleNaddrEvent(lessonId.split(':')[2]); + const author = await fetchAuthor(l.pubkey); + const parsedLesson = parseEvent(l); + const lessonObj = { + ...parsedLesson, + author + } + setLessons(prev => [...prev, lessonObj]); + } catch (error) { + console.error('Error fetching lesson:', error); + } + } + + lessonIds.forEach(lessonId => fetchLesson(lessonId)); + } + }, [lessonIds]); + + useEffect(() => { + console.log("AHHHHH", lessons); + }, [lessons]) + return ( -
-

{course?.name}

-

{course?.description}

+ <> + + { + + lessons.length > 0 && lessons.map((lesson, index) => ( +
+
+
+
+
+ {lesson && lesson.topics && lesson.topics.length > 0 && ( + lesson.topics.map((topic, index) => ( + + )) + ) + } +
+

{lesson?.title}

+

{lesson?.summary}

+
+ avatar thumbnail +

+ Created by{' '} + + {lesson.author?.username} + +

+
+
+
+ {lesson && ( +
+ resource thumbnail + {bitcoinConnect ? ( +
+ +
+ ) : ( +
+
+ )} +
+ )} +
+
+
+
+ { + lesson?.content && + } +
+
+ )) + }
{ course?.content && }
-
+ ); } diff --git a/src/pages/details/[slug].js b/src/pages/details/[slug].js index f0a14b9..17ec6b2 100644 --- a/src/pages/details/[slug].js +++ b/src/pages/details/[slug].js @@ -123,7 +123,7 @@ export default function Details() {

{processedEvent?.summary}

resource thumbnail {bitcoinConnect ? (
diff --git a/src/utils/nostr.js b/src/utils/nostr.js index debea0f..df480c5 100644 --- a/src/utils/nostr.js +++ b/src/utils/nostr.js @@ -52,6 +52,12 @@ export const parseEvent = (event) => { case 'summary': eventData.summary = tag[1]; break; + case 'description': + eventData.summary = tag[1]; + break; + case 'name': + eventData.title = tag[1]; + break; case 'image': eventData.image = tag[1]; break; @@ -61,6 +67,13 @@ export const parseEvent = (event) => { case 'author': eventData.author = tag[1]; break; + // How do we get topics / tags? + case 'l': + // Grab index 1 and any subsequent elements in the array + tag.slice(1).forEach(topic => { + eventData.topics.push(topic); + }); + break; case 'd': eventData.d = tag[1]; break;