Improved and simplified course form and handle submit

This commit is contained in:
austinkelsay 2024-07-22 16:11:46 -05:00
parent 45bc85bba6
commit 831e42d188
7 changed files with 165 additions and 256 deletions

View File

@ -31,7 +31,6 @@ const ContentDropdownItem = ({ content, onSelect, selected }) => {
return (
<div className="w-full border-t-2 border-gray-700 py-4">
<div className="flex flex-row gap-4 p-2">
{console.log(content)}
<Image
alt="content thumbnail"
src={returnImageProxy(content.image)}

View File

@ -48,6 +48,7 @@ const CourseForm = () => {
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: <ContentDropdownItem content={{ id, title, summary, image, published_at }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
label: <ContentDropdownItem content={{ id, kind, pubkey, content, title, summary, image, published_at, d, topics }} onSelect={(content) => 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: <ContentDropdownItem content={{ id, title, summary, image, published_at }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
label: <ContentDropdownItem content={{ id, kind, pubkey, content, title, summary, image, published_at, d, topics }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
value: id
};
});
@ -358,4 +329,4 @@ const CourseForm = () => {
);
}
export default CourseForm;
export default CourseForm;

View File

@ -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}`]),
],
});

View File

@ -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 || []);
}

View File

@ -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 (
<div className="flex flex-col justify-center mx-12">
<h1 className="my-6 text-3xl text-center font-bold">{course?.title}</h1>
<h2 className="text-lg text-center whitespace-pre-line">{course?.summary}</h2>
<h1 className="my-6 text-3xl text-center font-bold">{course?.name}</h1>
<h2 className="text-lg text-center whitespace-pre-line">{course?.description}</h2>
<div className="mx-auto my-6">
{
course?.content && <MDDisplay source={course.content} />

View File

@ -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"),
{

View File

@ -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);
}