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 ( return (
<div className="w-full border-t-2 border-gray-700 py-4"> <div className="w-full border-t-2 border-gray-700 py-4">
<div className="flex flex-row gap-4 p-2"> <div className="flex flex-row gap-4 p-2">
{console.log(content)}
<Image <Image
alt="content thumbnail" alt="content thumbnail"
src={returnImageProxy(content.image)} src={returnImageProxy(content.image)}

View File

@ -48,6 +48,7 @@ const CourseForm = () => {
setDrafts(drafts); setDrafts(drafts);
} }
if (resources.length > 0) { if (resources.length > 0) {
console.log('resources:', resources);
setResources(resources); setResources(resources);
} }
if (workshops.length > 0) { if (workshops.length > 0) {
@ -65,152 +66,122 @@ const CourseForm = () => {
} }
}, [user]); }, [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) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
// Set aside a list for all of the final ids in order const newCourseId = uuidv4();
const finalIds = []; const processedLessons = [];
// Iterate over selectedLessons and process each lesson try {
for (const lesson of selectedLessons) { // Step 1: Process lessons
if (lesson.published_at) { console.log('selectedLessons:', selectedLessons);
// If the lesson is already published, add its id to finalIds for (const lesson of selectedLessons) {
finalIds.push(lesson.id); let noteId = lesson.noteId;
} else {
// If the lesson is unpublished, create an event and sign it, publish it, save to db, and add its id to finalIds if (!lesson.published_at) {
let event; // Publish unpublished lesson
if (lesson.price) { const event = createLessonEvent(lesson);
event = { const signedEvent = await window.nostr.signEvent(event);
kind: 30402, const published = await publish(signedEvent);
content: lesson.content,
created_at: Math.floor(Date.now() / 1000), if (!published) {
tags: [ throw new Error(`Failed to publish lesson: ${lesson.title}`);
['d', lesson.id], }
['title', lesson.title],
['summary', lesson.summary], noteId = signedEvent.id;
['image', lesson.image],
...lesson.topics.map(topic => ['t', topic]), // Save to db and delete draft
['published_at', Math.floor(Date.now() / 1000).toString()], await Promise.all([
['price', lesson.price], axios.post('/api/resources', {
['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`], id: lesson.id,
] noteId: noteId,
}; userId: user.id,
} else { price: lesson.price || 0,
event = { }),
kind: 30023, axios.delete(`/api/drafts/${lesson.id}`)
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()]
]
};
} }
// Sign the event processedLessons.push({ id: lesson.d, noteId: lesson.id });
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);
});
}
} }
}
// Fetch all of the lessons from Nostr by their ids // Step 2: Create and publish course
const fetchedLessons = await Promise.all( const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, selectedLessons);
finalIds.map(async (id) => { const signedCourseEvent = await window.nostr.signEvent(courseEvent);
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
const published = await publish(signedCourseEvent); const published = await publish(signedCourseEvent);
if (published) { if (!published) {
axios.post('/api/courses', { throw new Error('Failed to publish course');
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.');
} }
// 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 handleLessonChange = (e, index) => {
const selectedLessonId = e.value; const selectedLessonId = e.value;
const selectedLesson = getContentOptions(index).flatMap(group => group.items).find(lesson => lesson.value === selectedLessonId); 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 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 { 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 value: id
}; };
}); });
const workshopOptions = workshops.map(workshop => { 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 { 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 value: id
}; };
}); });

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); setSummary(draft.summary);
setIsPaidResource(draft.price ? true : false); setIsPaidResource(draft.price ? true : false);
setPrice(draft.price || 0); setPrice(draft.price || 0);
setText(draft.content); setContent(draft.content);
setCoverImage(draft.image); setCoverImage(draft.image);
setTopics(draft.topics || []); setTopics(draft.topics || []);
} }

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useNostr } from "@/hooks/useNostr"; import { useNostr } from "@/hooks/useNostr";
import { parseEvent } from "@/utils/nostr"; import { parseCourseEvent } from "@/utils/nostr";
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
const MDDisplay = dynamic( const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"), () => import("@uiw/react-markdown-preview"),
@ -22,8 +22,7 @@ const Course = () => {
const getCourse = async () => { const getCourse = async () => {
if (slug) { if (slug) {
const fetchedCourse = await fetchSingleEvent(slug); const fetchedCourse = await fetchSingleEvent(slug);
console.log('fetched course:', fetchedCourse); const formattedCourse = parseCourseEvent(fetchedCourse);
const formattedCourse = parseEvent(fetchedCourse);
setCourse(formattedCourse); setCourse(formattedCourse);
} }
}; };
@ -35,8 +34,8 @@ const Course = () => {
return ( return (
<div className="flex flex-col justify-center mx-12"> <div className="flex flex-col justify-center mx-12">
<h1 className="my-6 text-3xl text-center font-bold">{course?.title}</h1> <h1 className="my-6 text-3xl text-center font-bold">{course?.name}</h1>
<h2 className="text-lg text-center whitespace-pre-line">{course?.summary}</h2> <h2 className="text-lg text-center whitespace-pre-line">{course?.description}</h2>
<div className="mx-auto my-6"> <div className="mx-auto my-6">
{ {
course?.content && <MDDisplay source={course.content} /> course?.content && <MDDisplay source={course.content} />

View File

@ -12,7 +12,6 @@ import Image from 'next/image';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
import dynamic from 'next/dynamic';
const MDDisplay = dynamic( const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"), () => import("@uiw/react-markdown-preview"),
{ {

View File

@ -75,6 +75,58 @@ export const parseEvent = (event) => {
return eventData; 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) => { export const hexToNpub = (hex) => {
return nip19.npubEncode(hex); return nip19.npubEncode(hex);
} }