Huge update with lessons and draftLessons and refactoring course forms, models, and api endpoints to account for these changes

This commit is contained in:
austinkelsay 2024-08-23 20:21:29 -05:00
parent 1165c6d7c8
commit 128234c7ad
17 changed files with 567 additions and 419 deletions

View File

@ -32,6 +32,32 @@ CREATE TABLE "Purchase" (
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id") CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "Lesson" (
"id" TEXT NOT NULL,
"courseId" TEXT NOT NULL,
"resourceId" TEXT,
"draftId" TEXT,
"index" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Lesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DraftLesson" (
"id" TEXT NOT NULL,
"courseDraftId" TEXT NOT NULL,
"resourceId" TEXT,
"draftId" TEXT,
"index" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DraftLesson_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "Course" ( CREATE TABLE "Course" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
@ -48,8 +74,6 @@ CREATE TABLE "Course" (
CREATE TABLE "Resource" ( CREATE TABLE "Resource" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"userId" TEXT NOT NULL, "userId" TEXT NOT NULL,
"courseId" TEXT,
"courseDraftId" TEXT,
"price" INTEGER NOT NULL DEFAULT 0, "price" INTEGER NOT NULL DEFAULT 0,
"noteId" TEXT, "noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -71,7 +95,6 @@ CREATE TABLE "Draft" (
"topics" TEXT[], "topics" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"courseDraftId" TEXT,
CONSTRAINT "Draft_pkey" PRIMARY KEY ("id") CONSTRAINT "Draft_pkey" PRIMARY KEY ("id")
); );
@ -87,7 +110,6 @@ CREATE TABLE "CourseDraft" (
"topics" TEXT[], "topics" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
"courseId" TEXT,
CONSTRAINT "CourseDraft_pkey" PRIMARY KEY ("id") CONSTRAINT "CourseDraft_pkey" PRIMARY KEY ("id")
); );
@ -104,9 +126,6 @@ CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId"); CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
-- CreateIndex
CREATE UNIQUE INDEX "CourseDraft_courseId_key" ON "CourseDraft"("courseId");
-- AddForeignKey -- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@ -119,26 +138,32 @@ ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_courseId_fkey" FOREIGN KEY ("cou
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_draftId_fkey" FOREIGN KEY ("draftId") REFERENCES "Draft"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DraftLesson" ADD CONSTRAINT "DraftLesson_courseDraftId_fkey" FOREIGN KEY ("courseDraftId") REFERENCES "CourseDraft"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DraftLesson" ADD CONSTRAINT "DraftLesson_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DraftLesson" ADD CONSTRAINT "DraftLesson_draftId_fkey" FOREIGN KEY ("draftId") REFERENCES "Draft"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Course" ADD CONSTRAINT "Course_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Course" ADD CONSTRAINT "Course_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- 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 -- AddForeignKey
ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Draft" ADD CONSTRAINT "Draft_courseDraftId_fkey" FOREIGN KEY ("courseDraftId") REFERENCES "CourseDraft"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -2,103 +2,124 @@ datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
pubkey String @unique pubkey String @unique
username String? @unique username String? @unique
avatar String? avatar String?
purchased Purchase[] purchased Purchase[]
courses Course[] courses Course[]
resources Resource[] resources Resource[]
courseDrafts CourseDraft[] courseDrafts CourseDraft[]
drafts Draft[] drafts Draft[]
role Role? @relation(fields: [roleId], references: [id]) role Role? @relation(fields: [roleId], references: [id])
roleId String? roleId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Role { model Role {
id String @id @default(uuid()) id String @id @default(uuid())
subscribed Boolean @default(false) subscribed Boolean @default(false)
users User[] users User[]
} }
model Purchase { model Purchase {
id String @id @default(uuid()) id String @id @default(uuid())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String userId String
course Course? @relation(fields: [courseId], references: [id]) course Course? @relation(fields: [courseId], references: [id])
courseId String? courseId String?
resource Resource? @relation(fields: [resourceId], references: [id]) resource Resource? @relation(fields: [resourceId], references: [id])
resourceId String? resourceId String?
amountPaid Int amountPaid Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
}
model Lesson {
id String @id @default(uuid())
courseId String
course Course @relation(fields: [courseId], references: [id])
resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id])
draftId String?
draft Draft? @relation(fields: [draftId], references: [id])
index Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DraftLesson {
id String @id @default(uuid())
courseDraftId String
courseDraft CourseDraft @relation(fields: [courseDraftId], references: [id])
resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id])
draftId String?
draft Draft? @relation(fields: [draftId], references: [id])
index Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }
model Course { model Course {
id String @id id String @id
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
price Int @default(0) price Int @default(0)
resources Resource[] lessons Lesson[]
purchases Purchase[] purchases Purchase[]
noteId String? @unique noteId String? @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
courseDraft CourseDraft?
} }
model Resource { model Resource {
id String @id // Client generates UUID id String @id // Client generates UUID
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
course Course? @relation(fields: [courseId], references: [id]) lessons Lesson[]
courseId String? draftLessons DraftLesson[]
courseDraft CourseDraft? @relation(fields: [courseDraftId], references: [id]) price Int @default(0)
courseDraftId String? purchases Purchase[]
price Int @default(0) noteId String? @unique
purchases Purchase[] createdAt DateTime @default(now())
noteId String? @unique updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }
model Draft { model Draft {
id String @id @default(uuid()) id String @id @default(uuid())
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
type String type String
title String title String
summary String summary String
content String content String
image String? image String?
price Int? @default(0) price Int? @default(0)
topics String[] topics String[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
courseDraft CourseDraft? @relation(fields: [courseDraftId], references: [id]) draftLessons DraftLesson[]
courseDraftId String? lessons Lesson[]
} }
model CourseDraft { model CourseDraft {
id String @id @default(uuid()) id String @id @default(uuid())
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
resources Resource[] draftLessons DraftLesson[]
drafts Draft[] title String
title String summary String
summary String image String?
image String? price Int? @default(0)
price Int? @default(0) topics String[]
topics String[] createdAt DateTime @default(now())
createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
updatedAt DateTime @updatedAt
course Course? @relation(fields: [courseId], references: [id])
courseId String? @unique
} }

View File

@ -7,10 +7,6 @@ import { Button } from "primereact/button";
const ContentDropdownItem = ({ content, onSelect }) => { const ContentDropdownItem = ({ content, onSelect }) => {
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
useEffect(() => {
console.log("content", content);
}, [content]);
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">

View File

@ -1,243 +0,0 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
import { InputText } from "primereact/inputtext";
import { InputNumber } from "primereact/inputnumber";
import { InputSwitch } from "primereact/inputswitch";
import { Button } from "primereact/button";
import { Dropdown } from "primereact/dropdown";
import { ProgressSpinner } from "primereact/progressspinner";
import { v4 as uuidv4 } from 'uuid';
import { useSession } from 'next-auth/react';
import { useNDKContext } from "@/context/NDKContext";
import { useRouter } from "next/router";
import { useToast } from "@/hooks/useToast";
import { useWorkshopsQuery } from "@/hooks/nostrQueries/content/useWorkshopsQuery";
import { useResourcesQuery } from "@/hooks/nostrQueries/content/useResourcesQuery";
import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { parseEvent } from "@/utils/nostr";
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
import SelectedContentItem from "@/components/content/SelectedContentItem";
import 'primeicons/primeicons.css';
// todo dont allow adding courses as resources
// todo need to update how I handle unpubbed resources
// todo add back topics
const CourseForm = ({ draft = null, isPublished = false }) => {
const [title, setTitle] = useState('');
const [summary, setSummary] = useState('');
const [isPaidCourse, setIsPaidCourse] = useState(draft?.price ? true : false);
const [price, setPrice] = useState(draft?.price || 0);
const [coverImage, setCoverImage] = useState('');
const [selectedContent, setSelectedContent] = useState([]);
const [topics, setTopics] = useState(['']);
const { resources, resourcesLoading, resourcesError } = useResourcesQuery();
const { workshops, workshopsLoading, workshopsError } = useWorkshopsQuery();
const { drafts, draftsLoading, draftsError } = useDraftsQuery();
const { data: session, status } = useSession();
const [user, setUser] = useState(null);
const { ndk, addSigner } = useNDKContext();
const router = useRouter();
const { showToast } = useToast();
useEffect(() => {
if (session) {
setUser(session.user);
}
}, [session]);
useEffect(() => {
if (draft) {
console.log('draft:', draft);
setTitle(draft.title);
setSummary(draft.summary);
setIsPaidCourse(draft.price > 0);
setPrice(draft.price || 0);
setCoverImage(draft.image);
setSelectedContent(draft.resources.concat(draft.drafts) || []);
setTopics(draft.topics || ['']);
}
}, [draft]);
const handlePriceChange = (value) => {
setPrice(value);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!user) {
showToast('error', 'Error', 'User not authenticated');
return;
}
try {
const courseDraftPayload = {
userId: user.id,
title,
summary,
image: coverImage,
price: price || 0,
topics,
resources: selectedContent.filter(content => content.kind === 30023 || content.kind === 30402).map(resource => resource.d),
drafts: selectedContent.filter(content => !content.kind).map(draft => draft.id),
};
const courseDraftResponse = await axios.post('/api/courses/drafts', courseDraftPayload);
const courseDraftId = courseDraftResponse.data.id;
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', error.response?.data?.details || error.message);
}
};
const handleContentSelect = (content) => {
if (!selectedContent.some(item => item.id === content.id)) {
setSelectedContent([...selectedContent, content]);
}
};
const removeContent = (index) => {
const updatedContent = selectedContent.filter((_, i) => i !== index);
setSelectedContent(updatedContent);
};
const addTopic = () => {
setTopics([...topics, '']);
};
const removeTopic = (index) => {
const updatedTopics = topics.filter((_, i) => i !== index);
setTopics(updatedTopics);
};
const handleTopicChange = (index, value) => {
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
setTopics(updatedTopics);
};
const getContentOptions = () => {
if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) {
return [];
}
const filterContent = (content) => {
const contentPrice = content.tags ? (content.tags.find(tag => tag[0] === 'price') ? parseInt(content.tags.find(tag => tag[0] === 'price')[1]) : 0) : (content.price || 0);
return isPaidCourse ? contentPrice > 0 : contentPrice === 0;
};
const draftOptions = drafts.filter(filterContent).map(draft => ({
label: draft.title,
value: draft
}));
const resourceOptions = resources.filter(filterContent).map(resource => {
const parsedResource = parseEvent(resource);
return {
label: parsedResource.title,
value: parsedResource
};
});
const workshopOptions = workshops.filter(filterContent).map(workshop => {
const parsedWorkshop = parseEvent(workshop);
return {
label: parsedWorkshop.title,
value: parsedWorkshop
};
});
return [
{
label: 'Drafts',
items: draftOptions
},
{
label: 'Resources',
items: resourceOptions
},
{
label: 'Workshops',
items: workshopOptions
}
];
};
if (resourcesLoading || workshopsLoading || draftsLoading) {
return <ProgressSpinner />;
}
return (
<form onSubmit={handleSubmit}>
<div className="p-inputgroup flex-1">
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
</div>
<div className="p-inputgroup flex-1 mt-4">
<InputText value={summary} onChange={(e) => setSummary(e.target.value)} placeholder="Summary" />
</div>
<div className="p-inputgroup flex-1 mt-4">
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
</div>
<div className="p-inputgroup flex-1 mt-8 flex-col">
<p className="py-2">Paid Course</p>
<InputSwitch checked={isPaidCourse} onChange={(e) => setIsPaidCourse(e.value)} />
{isPaidCourse && (
<div className="p-inputgroup flex-1 py-4">
<InputNumber
value={price}
onValueChange={(e) => handlePriceChange(e.value)}
placeholder="Price (sats)"
min={1}
/>
</div>
)}
</div>
<div className="mt-8 flex-col w-full">
<div className="mt-4 flex-col w-full">
{selectedContent.map((content, index) => (
<div key={content.id} className="flex mt-4">
<SelectedContentItem content={content} />
<Button
icon="pi pi-times"
className="p-button-danger rounded-tl-none rounded-bl-none"
onClick={() => removeContent(index)}
/>
</div>
))}
<div className="p-inputgroup flex-1 mt-4">
<Dropdown
options={getContentOptions()}
onChange={(e) => handleContentSelect(e.value)}
placeholder="Select Content"
itemTemplate={(option) => <ContentDropdownItem content={option.value} onSelect={handleContentSelect} />}
optionLabel="label"
optionGroupLabel="label"
optionGroupChildren="items"
/>
</div>
</div>
</div>
<div className="mt-4 flex-col w-full">
{topics.map((topic, index) => (
<div key={index} className="p-inputgroup flex-1 mt-4">
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder={`Topic #${index + 1}`} className="w-full" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={() => removeTopic(index)} />
)}
</div>
))}
<Button type="button" icon="pi pi-plus" onClick={addTopic} className="p-button-outlined mt-2" />
</div>
<div className="flex justify-center mt-8">
<Button type="submit" label="Create Draft" className="p-button-raised p-button-success" />
</div>
</form>
);
}
export default CourseForm;

View File

@ -20,7 +20,7 @@ const CourseForm = ({ draft = null }) => {
const [price, setPrice] = useState(draft?.price || 0); const [price, setPrice] = useState(draft?.price || 0);
const [coverImage, setCoverImage] = useState(draft?.image || ''); const [coverImage, setCoverImage] = useState(draft?.image || '');
const [topics, setTopics] = useState(draft?.topics || ['']); const [topics, setTopics] = useState(draft?.topics || ['']);
const [lessons, setLessons] = useState(draft?.resources || []); const [lessons, setLessons] = useState(draft?.resources?.map((resource, index) => ({ ...resource, index })) || []);
const [allContent, setAllContent] = useState([]); const [allContent, setAllContent] = useState([]);
const { data: session } = useSession(); const { data: session } = useSession();
@ -36,34 +36,59 @@ const CourseForm = ({ draft = null }) => {
} }
}, [resources, workshops, drafts, resourcesLoading, workshopsLoading, draftsLoading]); }, [resources, workshops, drafts, resourcesLoading, workshopsLoading, draftsLoading]);
const handleSubmit = async (e) => { const handleSubmit = async (event) => {
e.preventDefault(); event.preventDefault();
if (!session?.user) {
showToast('error', 'Error', 'User not authenticated');
return;
}
try { try {
const courseDraftPayload = { // First, create the courseDraft
userId: session?.user.id, const courseDraftData = {
userId: session.user.id,
title, title,
summary, summary,
image: coverImage, image: coverImage,
price: price || 0, price: isPaidCourse ? price : 0,
topics, topics,
resources: lessons.filter(content => content.kind === 30023 || content.kind === 30402).map(resource => resource.d),
drafts: lessons.filter(content => !content.kind).map(draft => draft.id),
}; };
const response = await axios.post('/api/courses/drafts', courseDraftPayload); console.log('courseDraftData', courseDraftData);
const courseDraftId = response.data.id;
showToast('success', 'Success', 'Course draft saved successfully'); const courseDraftResponse = await axios.post('/api/courses/drafts', courseDraftData);
router.push(`/course/${courseDraftId}/draft`); const createdCourseDraft = courseDraftResponse.data;
// Now create all the lessonDrafts with the courseDraftId
const createdLessonDrafts = await Promise.all(
lessons.map(async (lessonDraft, index) => {
console.log('lessonDraft', lessonDraft);
const isResource = lessonDraft?.kind ? true : false;
let payload = {};
if (isResource) {
payload = {
courseDraftId: createdCourseDraft.id,
resourceId: lessonDraft.d,
index: index
};
} else {
payload = {
courseDraftId: createdCourseDraft.id,
draftId: lessonDraft.id,
index: index
};
}
const response = await axios.post('/api/lessons/drafts', payload);
console.log('Lesson draft created:', response.data);
return response.data;
})
);
console.log('Course draft created:', createdCourseDraft);
console.log('Lesson drafts created:', createdLessonDrafts);
showToast('success', 'Success', 'Course draft created successfully');
router.push(`/course/${createdCourseDraft.id}/draft`);
} catch (error) { } catch (error) {
console.error('Error saving course draft:', error); console.error('Error creating course draft:', error);
showToast('error', 'Failed to save course draft', error.response?.data?.details || error.message); showToast('error', 'Error', 'Failed to create course draft');
} }
}; };

View File

@ -84,22 +84,23 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => {
const handleContentSelect = (selectedContent) => { const handleContentSelect = (selectedContent) => {
if (selectedContent && !lessons.some(lesson => lesson.id === selectedContent.id)) { if (selectedContent && !lessons.some(lesson => lesson.id === selectedContent.id)) {
setLessons([...lessons, selectedContent]); setLessons([...lessons, { ...selectedContent, index: lessons.length }]);
} }
}; };
const removeLesson = (index) => { const removeLesson = (index) => {
const updatedLessons = lessons.filter((_, i) => i !== index); const updatedLessons = lessons.filter((_, i) => i !== index)
.map((lesson, newIndex) => ({ ...lesson, index: newIndex }));
setLessons(updatedLessons); setLessons(updatedLessons);
}; };
const handleNewResourceSave = (newResource) => { const handleNewResourceSave = (newResource) => {
setLessons([...lessons, newResource]); setLessons([...lessons, { ...newResource, index: lessons.length }]);
setShowResourceForm(false); setShowResourceForm(false);
}; };
const handleNewWorkshopSave = (newWorkshop) => { const handleNewWorkshopSave = (newWorkshop) => {
setLessons([...lessons, newWorkshop]); setLessons([...lessons, { ...newWorkshop, index: lessons.length }]);
setShowWorkshopForm(false); setShowWorkshopForm(false);
}; };
@ -108,7 +109,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => {
<h3>Lessons</h3> <h3>Lessons</h3>
{lessons.map((lesson, index) => ( {lessons.map((lesson, index) => (
<div key={lesson.id} className="flex mt-4"> <div key={lesson.id} className="flex mt-4">
<SelectedContentItem content={lesson} /> <SelectedContentItem content={{ ...lesson, index }} />
<Button <Button
icon="pi pi-times" icon="pi pi-times"
className="p-button-danger rounded-tl-none rounded-bl-none" className="p-button-danger rounded-tl-none rounded-bl-none"

View File

@ -6,8 +6,15 @@ export const getAllCourseDraftsByUserId = async (userId) => {
where: { userId }, where: { userId },
include: { include: {
user: true, // Include the related user user: true, // Include the related user
resources: true, // Include related resources draftLessons: {
drafts: true, // Include related drafts include: {
draft: true,
resource: true
},
orderBy: {
index: 'asc'
}
},
}, },
}); });
}; };
@ -18,8 +25,15 @@ export const getCourseDraftById = async (id) => {
where: { id }, where: { id },
include: { include: {
user: true, // Include the related user user: true, // Include the related user
resources: true, // Include related resources draftLessons: {
drafts: true, // Include related drafts include: {
draft: true,
resource: true
},
orderBy: {
index: 'asc'
}
},
}, },
}); });
}; };
@ -29,35 +43,49 @@ export const createCourseDraft = async (data) => {
return await prisma.courseDraft.create({ return await prisma.courseDraft.create({
data: { data: {
...data, ...data,
resources: { user: { connect: { id: data.userId } },
connect: data.resources?.map((resource) => ({ id: resource.id })) || [],
},
drafts: {
connect: data.drafts?.map((draft) => ({ id: draft.id })) || [],
},
}, },
include: { include: {
resources: true, draftLessons: {
drafts: true, include: {
draft: true,
resource: true
},
orderBy: {
index: 'asc'
}
},
} }
}); });
}; };
// Update an existing CourseDraft by its ID // Update an existing CourseDraft by its ID
export const updateCourseDraft = async (id, data) => { export const updateCourseDraft = async (id, data) => {
const { resourceIds, draftIds, ...otherData } = data; const { draftLessons, ...otherData } = data;
return await prisma.courseDraft.update({ return await prisma.courseDraft.update({
where: { id }, where: { id },
data: { data: {
...otherData, ...otherData,
resources: { draftLessons: {
set: resourceIds?.map((resourceId) => ({ id: resourceId })) || [], deleteMany: {},
}, create: draftLessons.map((lesson, index) => ({
drafts: { draftId: lesson.draftId,
set: draftIds?.map((draftId) => ({ id: draftId })) || [], resourceId: lesson.resourceId,
}, index: index
}))
}
}, },
include: { resources: true, drafts: true } include: {
draftLessons: {
include: {
draft: true,
resource: true
},
orderBy: {
index: 'asc'
}
}
}
}); });
}; };

View File

@ -3,8 +3,16 @@ import prisma from "../prisma";
export const getAllCourses = async () => { export const getAllCourses = async () => {
return await prisma.course.findMany({ return await prisma.course.findMany({
include: { include: {
resources: true, // Include related resources lessons: {
purchases: true, // Include related purchases include: {
resource: true,
draft: true
},
orderBy: {
index: 'asc'
}
},
purchases: true,
}, },
}); });
}; };
@ -13,8 +21,16 @@ export const getCourseById = async (id) => {
return await prisma.course.findUnique({ return await prisma.course.findUnique({
where: { id }, where: { id },
include: { include: {
resources: true, // Include related resources lessons: {
purchases: true, // Include related purchases include: {
resource: true,
draft: true
},
orderBy: {
index: 'asc'
}
},
purchases: true,
}, },
}); });
}; };
@ -24,20 +40,53 @@ export const createCourse = async (data) => {
data: { data: {
id: data.id, id: data.id,
noteId: data.noteId, noteId: data.noteId,
user: data.user, user: { connect: { id: data.userId } },
resources: data.resources lessons: {
create: data.lessons.map((lesson, index) => ({
resourceId: lesson.resourceId,
draftId: lesson.draftId,
index: index
}))
}
}, },
include: { include: {
resources: true, lessons: {
include: {
resource: true,
draft: true
}
},
user: true user: true
} }
}); });
}; };
export const updateCourse = async (id, data) => { export const updateCourse = async (id, data) => {
const { lessons, ...otherData } = data;
return await prisma.course.update({ return await prisma.course.update({
where: { id }, where: { id },
data, data: {
...otherData,
lessons: {
deleteMany: {},
create: lessons.map((lesson, index) => ({
resourceId: lesson.resourceId,
draftId: lesson.draftId,
index: index
}))
}
},
include: {
lessons: {
include: {
resource: true,
draft: true
},
orderBy: {
index: 'asc'
}
}
}
}); });
}; };

View File

@ -0,0 +1,74 @@
import prisma from "../prisma";
export const getAllDraftLessons = async () => {
return await prisma.draftLesson.findMany({
include: {
courseDraft: true,
draft: true,
resource: true,
},
});
};
export const getDraftLessonById = async (id) => {
return await prisma.draftLesson.findUnique({
where: { id },
include: {
courseDraft: true,
draft: true,
resource: true,
},
});
};
export const createDraftLesson = async (data) => {
return await prisma.draftLesson.create({
data: {
courseDraftId: data.courseDraftId,
draftId: data.draftId,
resourceId: data.resourceId,
index: data.index,
},
include: {
courseDraft: true,
draft: true,
resource: true,
},
});
};
export const updateDraftLesson = async (id, data) => {
return await prisma.draftLesson.update({
where: { id },
data: {
courseDraftId: data.courseDraftId,
draftId: data.draftId,
resourceId: data.resourceId,
index: data.index,
},
include: {
courseDraft: true,
draft: true,
resource: true,
},
});
};
export const deleteDraftLesson = async (id) => {
return await prisma.draftLesson.delete({
where: { id },
});
};
export const getDraftLessonsByCourseDraftId = async (courseDraftId) => {
return await prisma.draftLesson.findMany({
where: { courseDraftId },
include: {
draft: true,
resource: true,
},
orderBy: {
index: 'asc',
},
});
};

View File

@ -0,0 +1,74 @@
import prisma from "../prisma";
export const getAllLessons = async () => {
return await prisma.lesson.findMany({
include: {
course: true,
resource: true,
draft: true,
},
});
};
export const getLessonById = async (id) => {
return await prisma.lesson.findUnique({
where: { id },
include: {
course: true,
resource: true,
draft: true,
},
});
};
export const createLesson = async (data) => {
return await prisma.lesson.create({
data: {
courseId: data.courseId,
resourceId: data.resourceId,
draftId: data.draftId,
index: data.index,
},
include: {
course: true,
resource: true,
draft: true,
},
});
};
export const updateLesson = async (id, data) => {
return await prisma.lesson.update({
where: { id },
data: {
courseId: data.courseId,
resourceId: data.resourceId,
draftId: data.draftId,
index: data.index,
},
include: {
course: true,
resource: true,
draft: true,
},
});
};
export const deleteLesson = async (id) => {
return await prisma.lesson.delete({
where: { id },
});
};
export const getLessonsByCourseId = async (courseId) => {
return await prisma.lesson.findMany({
where: { courseId },
include: {
resource: true,
draft: true,
},
orderBy: {
index: 'asc',
},
});
};

View File

@ -10,16 +10,7 @@ export default async function handler(req, res) {
try { try {
const courseDraft = await getCourseDraftById(slug); const courseDraft = await getCourseDraftById(slug);
// For now we will combine resources and drafts into one array res.status(200).json(courseDraft);
const courseDraftWithResources = {
...courseDraft,
resources: [...courseDraft.resources, ...courseDraft.drafts]
};
if (courseDraft) {
res.status(200).json(courseDraftWithResources);
} else {
res.status(404).json({ error: 'Course draft not found' });
}
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }

View File

@ -3,7 +3,7 @@ import prisma from "@/db/prisma";
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.method === 'POST') { if (req.method === 'POST') {
try { try {
const { userId, title, summary, image, price, topics, resources, drafts } = req.body; const { userId, title, summary, image, price, topics } = req.body;
if (!userId) { if (!userId) {
return res.status(400).json({ error: 'userId is required' }); return res.status(400).json({ error: 'userId is required' });
@ -17,14 +17,8 @@ export default async function handler(req, res) {
price, price,
topics: topics || [], topics: topics || [],
user: { connect: { id: userId } }, user: { connect: { id: userId } },
resources: {
connect: resources ? resources.map(id => ({ id })) : []
},
drafts: {
connect: drafts ? drafts.map(id => ({ id })) : []
}
}, },
include: { resources: true, drafts: true } include: { draftLessons: true }
}); });
res.status(201).json(courseDraft); res.status(201).json(courseDraft);

View File

@ -0,0 +1,35 @@
import { getLessonById, updateLesson, deleteLesson } from "@/db/models/lessonModels";
export default async function handler(req, res) {
const { slug } = req.query;
if (req.method === 'GET') {
try {
const lesson = await getLessonById(slug);
if (lesson) {
res.status(200).json(lesson);
} else {
res.status(404).json({ error: 'Lesson not found' });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
} else if (req.method === 'PUT') {
try {
const lesson = await updateLesson(slug, req.body);
res.status(200).json(lesson);
} catch (error) {
res.status(400).json({ error: error.message });
}
} else if (req.method === 'DELETE') {
try {
await deleteLesson(slug);
res.status(204).end();
} catch (error) {
res.status(500).json({ error: error.message });
}
} else {
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -0,0 +1,35 @@
import { getDraftLessonById, updateDraftLesson, deleteDraftLesson } from "@/db/models/draftLessonModels";
export default async function handler(req, res) {
const { slug } = req.query;
if (req.method === 'GET') {
try {
const draftLesson = await getDraftLessonById(slug);
if (draftLesson) {
res.status(200).json(draftLesson);
} else {
res.status(404).json({ error: 'Draft lesson not found' });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
} else if (req.method === 'PUT') {
try {
const draftLesson = await updateDraftLesson(slug, req.body);
res.status(200).json(draftLesson);
} catch (error) {
res.status(400).json({ error: error.message });
}
} else if (req.method === 'DELETE') {
try {
await deleteDraftLesson(slug);
res.status(204).end();
} catch (error) {
res.status(500).json({ error: error.message });
}
} else {
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -0,0 +1,22 @@
import { getAllDraftLessons, createDraftLesson } from "@/db/models/draftLessonModels";
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
const draftLessons = await getAllDraftLessons();
res.status(200).json(draftLessons);
} catch (error) {
res.status(500).json({ error: error.message });
}
} else if (req.method === 'POST') {
try {
const draftLesson = await createDraftLesson(req.body);
res.status(201).json(draftLesson);
} catch (error) {
res.status(400).json({ error: error.message });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -0,0 +1,22 @@
import { getAllLessons, createLesson } from "@/db/models/lessonModels";
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
const lessons = await getAllLessons();
res.status(200).json(lessons);
} catch (error) {
res.status(500).json({ error: error.message });
}
} else if (req.method === 'POST') {
try {
const lesson = await createLesson(req.body);
res.status(201).json(lesson);
} catch (error) {
res.status(400).json({ error: error.message });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -36,7 +36,7 @@ const DraftCourse = () => {
.then(res => { .then(res => {
setCourse(res.data); setCourse(res.data);
console.log('coursesssss:', res.data); console.log('coursesssss:', res.data);
setLessons(res.data.resources); // Set the raw lessons setLessons(res.data.draftLessons);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@ -47,21 +47,20 @@ const DraftCourse = () => {
useEffect(() => { useEffect(() => {
const fetchLessonDetails = async () => { const fetchLessonDetails = async () => {
if (lessons.length > 0) { if (lessons.length > 0) {
console.log('lessons in fetchLessonDetails', lessons);
await ndk.connect(); await ndk.connect();
const newLessonsWithAuthors = await Promise.all(lessons.map(async (lesson) => { const newLessonsWithAuthors = await Promise.all(lessons.map(async (lesson) => {
// figure out if it is a resource or a draft // figure out if it is a resource or a draft
const isDraft = !lesson.noteId; const isDraft = !lesson?.resource;
if (isDraft) { if (isDraft) {
const parsedLessonObject = { const parsedLessonObject = {
...lesson, ...lesson?.draft,
author: session.user author: session.user
} }
return parsedLessonObject; return parsedLessonObject;
} else { } else {
const filter = { const filter = {
"#d": [lesson.id] "#d": [lesson?.resource?.id]
}; };
const event = await ndk.fetchEvent(filter); const event = await ndk.fetchEvent(filter);