diff --git a/prisma/migrations/20241203184559_cascade_delete_user_lessons/migration.sql b/prisma/migrations/20241203184559_cascade_delete_user_lessons/migration.sql
new file mode 100644
index 0000000..3d6b23c
--- /dev/null
+++ b/prisma/migrations/20241203184559_cascade_delete_user_lessons/migration.sql
@@ -0,0 +1,5 @@
+-- DropForeignKey
+ALTER TABLE "UserLesson" DROP CONSTRAINT "UserLesson_lessonId_fkey";
+
+-- AddForeignKey
+ALTER TABLE "UserLesson" ADD CONSTRAINT "UserLesson_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "Lesson"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index faa87eb..855467b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -179,7 +179,7 @@ model UserLesson {
userId String
user User @relation(fields: [userId], references: [id])
lessonId String
- lesson Lesson @relation(fields: [lessonId], references: [id])
+ lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
opened Boolean @default(false)
completed Boolean @default(false)
openedAt DateTime?
diff --git a/src/components/content/courses/CourseDetails.js b/src/components/content/courses/CourseDetails.js
index fee1b5c..ddb4210 100644
--- a/src/components/content/courses/CourseDetails.js
+++ b/src/components/content/courses/CourseDetails.js
@@ -203,7 +203,7 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec
{renderPaymentMessage()}
{processedEvent?.pubkey === session?.user?.pubkey ? (
- router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
+ router.push(`/course/${processedEvent.d}/edit`)} label="Edit" severity='warning' outlined />
window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={isMobileView ? null : "View Nostr Event"} tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} />
diff --git a/src/components/forms/course/PublishedCourseForm.js b/src/components/forms/course/PublishedCourseForm.js
new file mode 100644
index 0000000..4346a5e
--- /dev/null
+++ b/src/components/forms/course/PublishedCourseForm.js
@@ -0,0 +1,227 @@
+import React, { useState, useEffect } from 'react';
+import { InputText } from 'primereact/inputtext';
+import { InputTextarea } from 'primereact/inputtextarea';
+import { InputNumber } from 'primereact/inputnumber';
+import { InputSwitch } from 'primereact/inputswitch';
+import GenericButton from '@/components/buttons/GenericButton';
+import LessonSelector from '@/components/forms/course/LessonSelector';
+import { parseEvent } from '@/utils/nostr';
+import { useRouter } from 'next/router';
+import { useNDKContext } from '@/context/NDKContext';
+import { useDraftsQuery } from '@/hooks/apiQueries/useDraftsQuery';
+import { useDocuments } from '@/hooks/nostr/useDocuments';
+import { useVideos } from '@/hooks/nostr/useVideos';
+import { useToast } from '@/hooks/useToast';
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import axios from 'axios';
+import dynamic from 'next/dynamic';
+
+const PublishedCourseForm = ({ course }) => {
+ const [title, setTitle] = useState(course?.name || '');
+ const [summary, setSummary] = useState(course?.description || '');
+ const [content, setContent] = useState(course?.content || '');
+ const [isPaidCourse, setIsPaidCourse] = useState(course?.price ? true : false);
+ const [price, setPrice] = useState(course?.price || 0);
+ const [coverImage, setCoverImage] = useState(course?.image || '');
+ const [topics, setTopics] = useState(course?.topics || ['']);
+ const [lessons, setLessons] = useState([]);
+ const [lessonIds, setLessonIds] = useState([]);
+ const [allContent, setAllContent] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ const router = useRouter();
+ const { ndk, addSigner } = useNDKContext();
+ const { showToast } = useToast();
+ const { documents, documentsLoading, documentsError } = useDocuments();
+ const { videos, videosLoading, videosError } = useVideos();
+ const { drafts, draftsLoading, draftsError } = useDraftsQuery();
+
+ useEffect(() => {
+ if (!documentsLoading && !videosLoading && !draftsLoading) {
+ let combinedContent = [];
+ if (documents) {
+ combinedContent = [...combinedContent, ...documents];
+ }
+ if (videos) {
+ combinedContent = [...combinedContent, ...videos];
+ }
+ if (drafts) {
+ combinedContent = [...combinedContent, ...drafts];
+ }
+ setAllContent(combinedContent);
+ }
+ }, [documents, videos, drafts, documentsLoading, videosLoading, draftsLoading]);
+
+ useEffect(() => {
+ if (course) {
+ const aTags = course.tags.filter(tag => tag[0] === 'a');
+ setLessonIds(aTags.map(tag => tag[1].split(':')[2]));
+ }
+ }, [course]);
+
+ useEffect(() => {
+ if (lessonIds.length > 0 && allContent.length > 0) {
+ // get all dtags from allContent
+ const dTags = allContent.map(content => content?.tags?.find(tag => tag[0] === 'd')?.[1]);
+ // filter lessonIds to only include dTags and grab those full objects from allContent and parse them
+ const lessons = lessonIds.filter(id => dTags.includes(id)).map(id => parseEvent(allContent.find(content => content?.tags?.find(tag => tag[0] === 'd')?.[1] === id)));
+ setLessons(lessons);
+ }
+ }, [lessonIds, allContent]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ if (!ndk.signer) {
+ await addSigner();
+ }
+ console.log('lessons', lessons);
+
+ const event = new NDKEvent(ndk);
+ event.kind = course.kind;
+ event.content = content;
+ event.tags = [
+ ['d', course.d],
+ ['name', title],
+ ['about', summary],
+ ['image', coverImage],
+ ...topics.filter(t => t.trim()).map(topic => ['t', topic.toLowerCase()]),
+ ['published_at', Math.floor(Date.now() / 1000).toString()],
+ ...(isPaidCourse ? [['price', price.toString()]] : []),
+ // Preserve existing lesson references
+ ...lessons.map(lesson => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`])
+ ];
+
+ await ndk.publish(event);
+
+ // Update course in database
+ await axios.put(`/api/courses/${course.d}`, {
+ price: isPaidCourse ? price : 0,
+ lessons: lessons.map(lesson => ({
+ resourceId: lesson.d,
+ draftId: null,
+ index: lessons.indexOf(lesson)
+ }))
+ });
+
+ showToast('success', 'Success', 'Course updated successfully');
+ router.push(`/course/${course.d}`);
+ } catch (error) {
+ console.error('Error updating course:', error);
+ showToast('error', 'Error', 'Failed to update course');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleTopicChange = (index, value) => {
+ const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
+ setTopics(updatedTopics);
+ };
+
+ const addTopic = (e) => {
+ e.preventDefault();
+ setTopics([...topics, '']);
+ };
+
+ const removeTopic = (e, index) => {
+ e.preventDefault();
+ const updatedTopics = topics.filter((_, i) => i !== index);
+ setTopics(updatedTopics);
+ };
+
+ return (
+
+ );
+};
+
+export default PublishedCourseForm;
\ No newline at end of file
diff --git a/src/db/models/courseModels.js b/src/db/models/courseModels.js
index c2039d9..9953ae4 100644
--- a/src/db/models/courseModels.js
+++ b/src/db/models/courseModels.js
@@ -62,8 +62,8 @@ export const updateCourse = async (id, data) => {
lessons: {
deleteMany: {},
create: lessons.map((lesson, index) => ({
- resourceId: lesson.resourceId,
- draftId: lesson.draftId,
+ resourceId: lesson.resourceId || lesson.d,
+ draftId: lesson.draftId || null,
index: index
}))
}
diff --git a/src/pages/course/[slug]/edit.js b/src/pages/course/[slug]/edit.js
new file mode 100644
index 0000000..b0b0ff9
--- /dev/null
+++ b/src/pages/course/[slug]/edit.js
@@ -0,0 +1,69 @@
+import React, { useState, useEffect } from "react";
+import { useRouter } from "next/router";
+import { useNDKContext } from "@/context/NDKContext";
+import { useSession } from 'next-auth/react';
+import { parseCourseEvent } from "@/utils/nostr";
+import { ProgressSpinner } from 'primereact/progressspinner';
+import PublishedCourseForm from "@/components/forms/course/PublishedCourseForm";
+import { useToast } from "@/hooks/useToast";
+
+const EditCourse = () => {
+ const [course, setCourse] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const router = useRouter();
+ const { ndk } = useNDKContext();
+ const { data: session } = useSession();
+ const { showToast } = useToast();
+
+ useEffect(() => {
+ if (!router.isReady || !session) return;
+
+ const fetchCourse = async () => {
+ try {
+ const { slug } = router.query;
+ await ndk.connect();
+ const event = await ndk.fetchEvent({ "#d": [slug] });
+
+ if (!event) {
+ showToast('error', 'Error', 'Course not found');
+ router.push('/dashboard');
+ return;
+ }
+
+ // Check if user is the author
+ if (event.pubkey !== session.user.pubkey) {
+ showToast('error', 'Error', 'Unauthorized');
+ router.push('/dashboard');
+ return;
+ }
+
+ const parsedCourse = parseCourseEvent(event);
+ setCourse(parsedCourse);
+ } catch (error) {
+ console.error('Error fetching course:', error);
+ showToast('error', 'Error', 'Failed to fetch course');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchCourse();
+ }, [router.isReady, router.query, ndk, session, showToast, router]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (!course) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+export default EditCourse;
\ No newline at end of file
diff --git a/src/utils/nostr.js b/src/utils/nostr.js
index 4c23cdd..680a7f8 100644
--- a/src/utils/nostr.js
+++ b/src/utils/nostr.js
@@ -163,6 +163,9 @@ export const parseCourseEvent = (event) => {
case 'description':
eventData.description = tag[1];
break;
+ case 'about':
+ eventData.description = tag[1];
+ break;
case 'image':
eventData.image = tag[1];
break;