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(`/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}
+
+
+
+ {lesson && (
+
+
+ {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}
{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;