diff --git a/src/components/content/dropdowns/ContentDropdownItem.js b/src/components/content/dropdowns/ContentDropdownItem.js index 52e27ee..718445e 100644 --- a/src/components/content/dropdowns/ContentDropdownItem.js +++ b/src/components/content/dropdowns/ContentDropdownItem.js @@ -31,7 +31,6 @@ const ContentDropdownItem = ({ content, onSelect, selected }) => { return (
- {console.log(content)} content thumbnail { setDrafts(drafts); } if (resources.length > 0) { + console.log('resources:', resources); setResources(resources); } if (workshops.length > 0) { @@ -65,152 +66,122 @@ const CourseForm = () => { } }, [user]); + /** + * Course Creation Flow: + * 1. Generate a new course ID + * 2. Process each lesson: + * - If unpublished: create event, publish to Nostr, save to DB, delete draft + * - If published: use existing data + * 3. Create and publish course event to Nostr + * 4. Save course to database + * 5. Show success message and redirect to course page + */ + const handleSubmit = async (e) => { e.preventDefault(); - // Set aside a list for all of the final ids in order - const finalIds = []; + const newCourseId = uuidv4(); + const processedLessons = []; - // Iterate over selectedLessons and process each lesson - for (const lesson of selectedLessons) { - if (lesson.published_at) { - // If the lesson is already published, add its id to finalIds - finalIds.push(lesson.id); - } else { - // If the lesson is unpublished, create an event and sign it, publish it, save to db, and add its id to finalIds - let event; - if (lesson.price) { - event = { - kind: 30402, - content: lesson.content, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['d', lesson.id], - ['title', lesson.title], - ['summary', lesson.summary], - ['image', lesson.image], - ...lesson.topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()], - ['price', lesson.price], - ['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`], - ] - }; - } else { - event = { - kind: 30023, - content: lesson.content, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['d', lesson.id], - ['title', lesson.title], - ['summary', lesson.summary], - ['image', lesson.image], - ...lesson.topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()] - ] - }; + try { + // Step 1: Process lessons + console.log('selectedLessons:', selectedLessons); + for (const lesson of selectedLessons) { + let noteId = lesson.noteId; + + if (!lesson.published_at) { + // Publish unpublished lesson + const event = createLessonEvent(lesson); + const signedEvent = await window.nostr.signEvent(event); + const published = await publish(signedEvent); + + if (!published) { + throw new Error(`Failed to publish lesson: ${lesson.title}`); + } + + noteId = signedEvent.id; + + // Save to db and delete draft + await Promise.all([ + axios.post('/api/resources', { + id: lesson.id, + noteId: noteId, + userId: user.id, + price: lesson.price || 0, + }), + axios.delete(`/api/drafts/${lesson.id}`) + ]); } - // Sign the event - const signedEvent = await window?.nostr?.signEvent(event); - - // Add the signed event's id to finalIds - finalIds.push(signedEvent.id); - - const published = await publish(signedEvent); - - if (published) { - // need to save resource to db - - // delete the draft - axios.delete(`/api/drafts/${lesson.id}`) - .then((response) => { - console.log('Draft deleted:', response); - }) - .catch((error) => { - console.error('Error deleting draft:', error); - }); - } + processedLessons.push({ id: lesson.d, noteId: lesson.id }); } - } - // Fetch all of the lessons from Nostr by their ids - const fetchedLessons = await Promise.all( - finalIds.map(async (id) => { - const lesson = await fetchSingleEvent(id); - console.log('got lesson:', lesson); - return lesson; - }) - ); - - // // Parse the fields from the lessons to get all of the necessary information - const parsedLessons = fetchedLessons.map((lesson) => { - const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(lesson); - return { - id, - kind, - pubkey, - content, - title, - summary, - image, - published_at, - d, - topics - }; - }); - - if (parsedLessons.length === selectedLessons.length) { - // Create a new course event - const newCourseId = uuidv4(); - const courseEvent = { - kind: 30004, - created_at: Math.floor(Date.now() / 1000), - content: "", - tags: [ - ['d', newCourseId], - // add a tag for plebdevs community at some point - ['name', title], - ['picture', coverImage], - ['image', coverImage], - ['description', summary], - ['l', "Education"], - ...parsedLessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]), - ], - }; - - // Sign the course event - const signedCourseEvent = await window?.nostr?.signEvent(courseEvent); - console.log('signedCourseEvent:', signedCourseEvent); - // Publish the course event using Nostr + // Step 2: Create and publish course + const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, selectedLessons); + const signedCourseEvent = await window.nostr.signEvent(courseEvent); const published = await publish(signedCourseEvent); - if (published) { - axios.post('/api/courses', { - id: newCourseId, - resources: { - connect: parsedLessons.map(lesson => ({ id: lesson.id })) - }, - noteId: signedCourseEvent.id, - user: { - connect: { - id: user.id - } - } - }) - .then(response => { - console.log('Course created:', response); - router.push(`/course/${signedCourseEvent.id}`); - }) - .catch(error => { - console.error('Error creating course:', error); - }); - } else { - showToast('error', 'Error', 'Failed to publish course. Please try again.'); + if (!published) { + throw new Error('Failed to publish course'); } + + // Step 3: Save course to db + console.log('processedLessons:', processedLessons); + await axios.post('/api/courses', { + id: newCourseId, + resources: { + connect: processedLessons.map(lesson => ({ id: lesson?.id })) + }, + noteId: signedCourseEvent.id, + user: { + connect: { id: user.id } + }, + price: price || 0 + }); + + // Step 4: Show success message and redirect + showToast('success', 'Course created successfully'); + router.push(`/course/${signedCourseEvent.id}`); + + } catch (error) { + console.error('Error creating course:', error); + showToast('error', error.message || 'Failed to create course. Please try again.'); } }; + const createLessonEvent = (lesson) => ({ + kind: lesson.price ? 30402 : 30023, + content: lesson.content, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['d', lesson.id], + ['title', lesson.title], + ['summary', lesson.summary], + ['image', lesson.image], + ...lesson.topics.map(topic => ['t', topic]), + ['published_at', Math.floor(Date.now() / 1000).toString()], + ...(lesson.price ? [ + ['price', lesson.price], + ['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`] + ] : []) + ] + }); + + const createCourseEvent = (courseId, title, summary, coverImage, lessons) => ({ + kind: 30004, + created_at: Math.floor(Date.now() / 1000), + content: "", + tags: [ + ['d', courseId], + ['name', title], + ['picture', coverImage], + ['image', coverImage], + ['description', summary], + ['l', "Education"], + ...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]), + ], + }); + const handleLessonChange = (e, index) => { const selectedLessonId = e.value; const selectedLesson = getContentOptions(index).flatMap(group => group.items).find(lesson => lesson.value === selectedLessonId); @@ -263,17 +234,17 @@ const CourseForm = () => { })); const resourceOptions = resources.map(resource => { - const { id, title, summary, image, published_at } = parseEvent(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} />, + label: handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />, value: id }; }); const workshopOptions = workshops.map(workshop => { - const { id, title, summary, image, published_at } = parseEvent(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} />, + label: handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />, value: id }; }); @@ -358,4 +329,4 @@ const CourseForm = () => { ); } -export default CourseForm; +export default CourseForm; \ No newline at end of file diff --git a/src/components/forms/CourseFormNew.js b/src/components/forms/CourseFormNew.js deleted file mode 100644 index a77a20b..0000000 --- a/src/components/forms/CourseFormNew.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Course Creation Flow: - * 1. Generate a new course ID - * 2. Process each lesson: - * - If unpublished: create event, publish to Nostr, save to DB, delete draft - * - If published: use existing data - * 3. Create and publish course event to Nostr - * 4. Save course to database - * 5. Show success message and redirect to course page - */ - -const handleSubmit = async (e) => { - e.preventDefault(); - - const newCourseId = uuidv4(); - const processedLessons = []; - - try { - // Step 1: Process lessons - for (const lesson of selectedLessons) { - let noteId = lesson.noteId; - - if (!lesson.published_at) { - // Publish unpublished lesson - const event = createLessonEvent(lesson); - const signedEvent = await window.nostr.signEvent(event); - const published = await publish(signedEvent); - - if (!published) { - throw new Error(`Failed to publish lesson: ${lesson.title}`); - } - - noteId = signedEvent.id; - - // Save to db and delete draft - await Promise.all([ - axios.post('/api/resources', { - id: lesson.id, - noteId: noteId, - userId: user.id, - price: lesson.price || 0, - }), - axios.delete(`/api/drafts/${lesson.id}`) - ]); - } - - processedLessons.push({ id: lesson.id, noteId: noteId }); - } - - // Step 2: Create and publish course - const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, processedLessons); - const signedCourseEvent = await window.nostr.signEvent(courseEvent); - const published = await publish(signedCourseEvent); - - if (!published) { - throw new Error('Failed to publish course'); - } - - // Step 3: Save course to db - await axios.post('/api/courses', { - id: newCourseId, - resources: { - connect: processedLessons.map(lesson => ({ id: lesson.id })) - }, - noteId: signedCourseEvent.id, - userId: user.id, - price: price || 0 - }); - - // Step 4: Show success message and redirect - showToast('success', 'Course created successfully'); - router.push(`/course/${newCourseId}`); - - } catch (error) { - console.error('Error creating course:', error); - showToast('error', error.message || 'Failed to create course. Please try again.'); - } -}; - -const createLessonEvent = (lesson) => ({ - kind: lesson.price ? 30402 : 30023, - content: lesson.content, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['d', lesson.id], - ['title', lesson.title], - ['summary', lesson.summary], - ['image', lesson.image], - ...lesson.topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()], - ...(lesson.price ? [ - ['price', lesson.price], - ['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`] - ] : []) - ] -}); - -const createCourseEvent = (courseId, title, summary, coverImage, lessons) => ({ - kind: 30004, - created_at: Math.floor(Date.now() / 1000), - content: "", - tags: [ - ['d', courseId], - ['name', title], - ['picture', coverImage], - ['image', coverImage], - ['description', summary], - ['l', "Education"], - ...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.id}`]), - ], -}); \ No newline at end of file diff --git a/src/components/forms/ResourceForm.js b/src/components/forms/ResourceForm.js index c5039a3..f1287b8 100644 --- a/src/components/forms/ResourceForm.js +++ b/src/components/forms/ResourceForm.js @@ -42,7 +42,7 @@ const ResourceForm = ({ draft = null }) => { setSummary(draft.summary); setIsPaidResource(draft.price ? true : false); setPrice(draft.price || 0); - setText(draft.content); + setContent(draft.content); setCoverImage(draft.image); setTopics(draft.topics || []); } diff --git a/src/pages/course/[slug].js b/src/pages/course/[slug].js index 5e7c2fb..0b95997 100644 --- a/src/pages/course/[slug].js +++ b/src/pages/course/[slug].js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { useNostr } from "@/hooks/useNostr"; -import { parseEvent } from "@/utils/nostr"; +import { parseCourseEvent } from "@/utils/nostr"; import dynamic from 'next/dynamic'; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -22,8 +22,7 @@ const Course = () => { const getCourse = async () => { if (slug) { const fetchedCourse = await fetchSingleEvent(slug); - console.log('fetched course:', fetchedCourse); - const formattedCourse = parseEvent(fetchedCourse); + const formattedCourse = parseCourseEvent(fetchedCourse); setCourse(formattedCourse); } }; @@ -35,8 +34,8 @@ const Course = () => { return (
-

{course?.title}

-

{course?.summary}

+

{course?.name}

+

{course?.description}

{ course?.content && diff --git a/src/pages/details/[slug].js b/src/pages/details/[slug].js index 7b595cc..f0a14b9 100644 --- a/src/pages/details/[slug].js +++ b/src/pages/details/[slug].js @@ -12,7 +12,6 @@ import Image from 'next/image'; import dynamic from 'next/dynamic'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import 'primeicons/primeicons.css'; -import dynamic from 'next/dynamic'; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), { diff --git a/src/utils/nostr.js b/src/utils/nostr.js index 2cbb461..8771f28 100644 --- a/src/utils/nostr.js +++ b/src/utils/nostr.js @@ -75,6 +75,58 @@ export const parseEvent = (event) => { return eventData; }; +export const parseCourseEvent = (event) => { + console.log('event:', event); + // Initialize an object to store the extracted data + const eventData = { + id: event.id, + pubkey: event.pubkey || '', + content: event.content || '', + kind: event.kind || '', + name: '', + description: '', + image: '', + published_at: '', + topics: [], + d: '', + }; + + // Iterate over the tags array to extract data + event.tags.forEach(tag => { + switch (tag[0]) { // Check the key in each key-value pair + case 'name': + eventData.name = tag[1]; + break; + case 'description': + eventData.description = tag[1]; + break; + case 'image': + eventData.image = tag[1]; + break; + case 'picture': + eventData.image = tag[1]; + break; + case 'published_at': + eventData.published_at = tag[1]; + break; + case 'd': + eventData.d = 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; + default: + break; + } + }); + + return eventData; +} + export const hexToNpub = (hex) => { return nip19.npubEncode(hex); }