diff --git a/prisma/migrations/20240809152643_init/migration.sql b/prisma/migrations/20240816152315_init/migration.sql similarity index 86% rename from prisma/migrations/20240809152643_init/migration.sql rename to prisma/migrations/20240816152315_init/migration.sql index 5c116ab..ba97517 100644 --- a/prisma/migrations/20240809152643_init/migration.sql +++ b/prisma/migrations/20240816152315_init/migration.sql @@ -49,6 +49,7 @@ CREATE TABLE "Resource" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "courseId" TEXT, + "courseDraftId" TEXT, "price" INTEGER NOT NULL DEFAULT 0, "noteId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -82,18 +83,14 @@ CREATE TABLE "CourseDraft" ( "summary" TEXT NOT NULL, "image" TEXT, "price" INTEGER DEFAULT 0, + "topics" TEXT[], "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, + "courseId" TEXT, CONSTRAINT "CourseDraft_pkey" PRIMARY KEY ("id") ); --- CreateTable -CREATE TABLE "_CourseDraftToResource" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - -- CreateIndex CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey"); @@ -107,10 +104,7 @@ CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId"); CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId"); -- CreateIndex -CREATE UNIQUE INDEX "_CourseDraftToResource_AB_unique" ON "_CourseDraftToResource"("A", "B"); - --- CreateIndex -CREATE INDEX "_CourseDraftToResource_B_index" ON "_CourseDraftToResource"("B"); +CREATE UNIQUE INDEX "CourseDraft_courseId_key" ON "CourseDraft"("courseId"); -- AddForeignKey ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE; @@ -133,6 +127,9 @@ ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userI -- AddForeignKey ALTER TABLE "Resource" ADD CONSTRAINT "Resource_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "Resource" ADD CONSTRAINT "Resource_courseDraftId_fkey" FOREIGN KEY ("courseDraftId") REFERENCES "CourseDraft"("id") ON DELETE SET NULL ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; @@ -140,7 +137,4 @@ ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") RE ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_CourseDraftToResource" ADD CONSTRAINT "_CourseDraftToResource_A_fkey" FOREIGN KEY ("A") REFERENCES "CourseDraft"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_CourseDraftToResource" ADD CONSTRAINT "_CourseDraftToResource_B_fkey" FOREIGN KEY ("B") REFERENCES "Resource"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1710cf5..52d35c4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,7 +2,6 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") } - generator client { provider = "prisma-client-js" } @@ -13,10 +12,10 @@ model User { username String? @unique avatar String? purchased Purchase[] - courses Course[] // Relation field added for courses created by the user - resources Resource[] // Relation field added for resources created by the user - drafts Draft[] // Relation field added for drafts created by the user - courseDrafts CourseDraft[] // Relation field added for course drafts created by the user + courses Course[] + resources Resource[] + courseDrafts CourseDraft[] + drafts Draft[] role Role? @relation(fields: [roleId], references: [id]) roleId String? createdAt DateTime @default(now()) @@ -37,13 +36,13 @@ model Purchase { courseId String? resource Resource? @relation(fields: [resourceId], references: [id]) resourceId String? - amountPaid Int // in satoshis + amountPaid Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Course { - id String @id // Client generates UUID + id String @id userId String user User @relation(fields: [userId], references: [id]) price Int @default(0) @@ -52,20 +51,22 @@ model Course { noteId String? @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + courseDraft CourseDraft? } model Resource { - id String @id // Client generates UUID - userId String - user User @relation(fields: [userId], references: [id]) - course Course? @relation(fields: [courseId], references: [id]) - courseId String? - price Int @default(0) - purchases Purchase[] - courseDrafts CourseDraft[] - noteId String? @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id // Client generates UUID + userId String + user User @relation(fields: [userId], references: [id]) + course Course? @relation(fields: [courseId], references: [id]) + courseId String? + courseDraft CourseDraft? @relation(fields: [courseDraftId], references: [id]) + courseDraftId String? + price Int @default(0) + purchases Purchase[] + noteId String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Draft { @@ -84,14 +85,17 @@ model Draft { } model CourseDraft { - id String @id @default(uuid()) + id String @id @default(uuid()) userId String - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) resources Resource[] title String summary String image String? - price Int? @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} + price Int? @default(0) + topics String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + course Course? @relation(fields: [courseId], references: [id]) + courseId String? @unique +} \ No newline at end of file diff --git a/src/components/content/courses/DraftCourseDetails.js b/src/components/content/courses/DraftCourseDetails.js index dfa8f11..52e510a 100644 --- a/src/components/content/courses/DraftCourseDetails.js +++ b/src/components/content/courses/DraftCourseDetails.js @@ -54,7 +54,14 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) }, [session]); const handleDelete = () => { - console.log('delete'); + axios.delete(`/api/courses/drafts/${processedEvent.id}`) + .then(() => { + showToast('success', 'Success', 'Draft Course deleted successfully'); + router.push('/'); + }) + .catch((error) => { + showToast('error', 'Error', 'Failed to delete draft course'); + }); } const handleSubmit = async (e) => { diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 2ff2000..0c55e22 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -19,6 +19,9 @@ import { parseEvent } from "@/utils/nostr"; import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem"; import 'primeicons/primeicons.css'; + +// todo dont allow adding courses as resources +// todo need to update how I handle unpubbed resources const CourseForm = ({ draft = null, isPublished = false }) => { const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); @@ -96,140 +99,37 @@ const CourseForm = ({ draft = null, isPublished = false }) => { const handleSubmit = async (e) => { e.preventDefault(); - if (!ndk.signer) { - await addSigner(); + if (!user) { + showToast('error', 'Error', 'User not authenticated'); + return; } - // Prepare the lessons from selected lessons - const resources = await Promise.all(selectedLessons.map(async (lesson) => { - if (lesson?.type) { - const event = createLessonEvent(lesson); - const published = await event.publish(); - - if (!published) { - throw new Error(`Failed to publish lesson: ${lesson.title}`); - } - - const resource = await axios.post('/api/resources', { - id: event.tags.find(tag => tag[0] === 'd')[1], - userId: user.id, - price: lesson.price || 0, - noteId: event.id, - }); - - if (resource.status !== 201) { - throw new Error(`Failed to post resource: ${lesson.title}`); - } - - const deleted = await axios.delete(`/api/drafts/${lesson.id}`); - - if (deleted.status !== 204) { - throw new Error(`Failed to delete draft: ${lesson.title}`); - } - - return { - id: lesson.id, - userId: user.id, - price: lesson.price || 0, - noteId: event.id, - } - } else { - return { - id: lesson.d, - userId: user.id, - price: lesson.price || 0, - noteId: lesson.id, - } - } - })); - - // if this is a draft any added resources should be updated with courseId - if (draft) { - resources.forEach(resource => { - console.log('each resource:', resource); - if (!draft.resources.includes(resource.id)) { - axios.put(`/api/resources/${resource.id}`, { courseId: draft.id }) - .then(response => { - console.log('resource updated:', response); - }) - .catch(error => { - console.error('error updating resource:', error); - }); - } - }); - } - - const payload = { - user: { - connect: { id: user.id }, - }, - title, - summary, - image: coverImage, - price: price || 0, - resources: { - set: resources.map(resource => ({ id: resource.id })), - }, - topics, - }; - try { - let response; - if (draft) { - // payload minus topics - delete payload.topics - response = await axios.put(`/api/courses/drafts/${draft.id}`, payload); - showToast('success', 'Success', 'Course draft updated successfully'); - } else { - response = await axios.post('/api/courses/drafts', payload); - showToast('success', 'Success', 'Course draft saved successfully'); + // Step 1: Create the course draft + const courseDraftPayload = { + userId: user.id, // Make sure this is set + title, + summary, + image: coverImage, + price: checked ? price : 0, + topics, + }; + + const courseDraftResponse = await axios.post('/api/courses/drafts', courseDraftPayload); + const courseDraftId = courseDraftResponse.data.id; + + // Step 2: Associate resources with the course draft + for (const lesson of selectedLessons) { + await axios.put(`/api/resources/${lesson.d}`, { + courseDraftId: courseDraftId + }); } - console.log('response:', response); - // router.push(`/course/${response.data.id}/draft`); + + showToast('success', 'Success', 'Course draft saved successfully'); + router.push(`/course/${courseDraftId}/draft`); } catch (error) { console.error('Error saving course draft:', error); - showToast('error', 'Failed to save course draft. Please try again.'); - } - }; - - const handlePublishedCourse = async (e) => { - e.preventDefault(); - - if (!ndk.signer) { - await addSigner(); - } - - const event = new NDKEvent(ndk); - event.kind = price > 0 ? 30402 : 30023; - event.content = JSON.stringify({ - title, - summary, - image: coverImage, - resources: selectedLessons.map(lesson => lesson.id), - }); - event.tags = [ - ['d', draft.id], - ['title', title], - ['summary', summary], - ['image', coverImage], - ...topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()], - ['price', price.toString()], - ]; - - try { - const published = await ndk.publish(event); - - if (published) { - const response = await axios.put(`/api/courses/${draft.id}`, { noteId: event.id }); - showToast('success', 'Success', 'Course published successfully'); - router.push(`/course/${event.id}`); - } else { - showToast('error', 'Error', 'Failed to publish course. Please try again.'); - } - } catch (error) { - console.error('Error publishing course:', error); - showToast('error', 'Failed to publish course. Please try again.'); + showToast('error', 'Failed to save course draft', error.response?.data?.details || error.message); } }; @@ -340,7 +240,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => { } return ( -
+
setTitle(e.target.value)} placeholder="Title" />
@@ -398,7 +298,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {