mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 08:42:02 +00:00
Improved and simplified course form and handle submit
This commit is contained in:
parent
45bc85bba6
commit
831e42d188
@ -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)}
|
||||
|
@ -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;
|
@ -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}`]),
|
||||
],
|
||||
});
|
@ -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 || []);
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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"),
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user