Course publishing works for free, paid, drafts, published and any combination of them

This commit is contained in:
austinkelsay 2024-08-25 12:12:55 -05:00
parent 8bd28d1a6a
commit 61a78f4b28
8 changed files with 79 additions and 48 deletions

View File

@ -35,7 +35,7 @@ CREATE TABLE "Purchase" (
-- CreateTable -- CreateTable
CREATE TABLE "Lesson" ( CREATE TABLE "Lesson" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"courseId" TEXT NOT NULL, "courseId" TEXT,
"resourceId" TEXT, "resourceId" TEXT,
"draftId" TEXT, "draftId" TEXT,
"index" INTEGER NOT NULL, "index" INTEGER NOT NULL,
@ -140,7 +140,7 @@ ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_courseId_fkey" FOREIGN KEY ("cou
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 -- AddForeignKey
ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Lesson" ADD CONSTRAINT "Lesson_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -44,8 +44,8 @@ model Purchase {
model Lesson { model Lesson {
id String @id @default(uuid()) id String @id @default(uuid())
courseId String courseId String?
course Course @relation(fields: [courseId], references: [id]) course Course? @relation(fields: [courseId], references: [id])
resourceId String? resourceId String?
resource Resource? @relation(fields: [resourceId], references: [id]) resource Resource? @relation(fields: [resourceId], references: [id])
draftId String? draftId String?

View File

@ -73,6 +73,27 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
}); });
} }
const handlePostLesson = async (lesson) => {
console.log('lesson in handlePostLesson', lesson);
let payload;
if (lesson.d) {
payload = {
resourceId: lesson.d,
index: lesson.index
}
} else if (lesson.draftId) {
payload = {
draftId: lesson.draftId,
index: lesson.index
}
}
const response = await axios.post(`/api/lessons`, payload);
return response.data;
}
const handlePostResource = async (resource) => { const handlePostResource = async (resource) => {
console.log('resourceeeeee:', resource.tags); console.log('resourceeeeee:', resource.tags);
const dTag = resource.tags.find(tag => tag[0] === 'd')[1]; const dTag = resource.tags.find(tag => tag[0] === 'd')[1];
@ -125,64 +146,64 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
await addSigner(); await addSigner();
} }
// Step 1: Process lessons // Step 1: Process lessons
const createdLessons = [];
for (const lesson of processedLessons) { for (const lesson of processedLessons) {
// publish any draft lessons and delete draft lessons let savedLesson;
const unpublished = lesson?.unpublished; if (lesson.unpublished) {
if (unpublished && Object.keys(unpublished).length > 0) { const validationResult = validateEvent(lesson.unpublished);
const validationResult = validateEvent(unpublished);
if (validationResult !== true) { if (validationResult !== true) {
console.error('Invalid event:', validationResult); console.error('Invalid event:', validationResult);
showToast('error', 'Error', `Invalid event: ${validationResult}`); showToast('error', 'Error', `Invalid event: ${validationResult}`);
return; return;
} }
const published = await unpublished.publish(); const published = await lesson.unpublished.publish();
savedLesson = await handlePostResource(lesson.unpublished);
const saved = await handlePostResource(unpublished); if (published && savedLesson) {
const deleted = await axios.delete(`/api/drafts/${lesson.d}`);
console.log('saved', saved); if (deleted && deleted.status === 204) {
const savedLesson = await handlePostLesson(lesson);
if (published && saved) { if (savedLesson) {
axios.delete(`/api/drafts/${lesson?.d}`) createdLessons.push(savedLesson);
.then(res => { }
if (res.status === 204) { }
showToast('success', 'Success', 'Draft deleted successfully.'); }
} else { } else {
showToast('error', 'Error', 'Failed to delete draft.'); const savedLesson = await handlePostLesson(lesson);
} if (savedLesson) {
}) createdLessons.push(savedLesson);
.catch(err => {
console.error(err);
});
} }
} }
} }
console.log('createdLessons', createdLessons);
// Step 2: Create and publish course // Step 2: Create and publish course
const courseEvent = createCourseEvent(newCourseId, processedEvent.title, processedEvent.summary, processedEvent.image, processedLessons, processedEvent.price); const courseEvent = createCourseEvent(newCourseId, processedEvent.title, processedEvent.summary, processedEvent.image, processedLessons, processedEvent.price);
const published = await courseEvent.publish(); const published = await courseEvent.publish();
console.log('published', published);
if (!published) { if (!published) {
throw new Error('Failed to publish course'); throw new Error('Failed to publish course');
} }
// Step 3: Save course to db // Step 3: Save course to db
await axios.post('/api/courses', { const courseData = {
id: newCourseId, id: newCourseId,
resources: { lessons: {
connect: processedLessons.map(lesson => ({ id: lesson?.d })) connect: createdLessons.map(lesson => ({ id: lesson.id }))
}, },
noteId: courseEvent.id, noteId: courseEvent.id,
user: { user: {
connect: { id: user.id } connect: { id: user.id }
}, },
price: processedEvent?.price || 0 price: processedEvent?.price || 0
}); };
// step 4: Update all resources to have the course id const createdCourse = await axios.post('/api/courses', courseData);
await Promise.all(processedLessons.map(lesson => axios.put(`/api/resources/${lesson?.d}`, { courseId: newCourseId })));
// Step 4: Update all lessons to have the course id
await Promise.all(createdLessons.map(lesson => axios.put(`/api/lessons/${lesson.id}`, { courseId: newCourseId })));
// Step 5: Delete draft // Step 5: Delete draft
await axios.delete(`/api/courses/drafts/${processedEvent.id}`); await axios.delete(`/api/courses/drafts/${processedEvent.id}`);
@ -292,13 +313,15 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
d: lesson?.id, d: lesson?.id,
kind: lesson?.price ? 30402 : 30023, kind: lesson?.price ? 30402 : 30023,
pubkey: unsignedEvent.pubkey, pubkey: unsignedEvent.pubkey,
index: lesson.index,
unpublished: unsignedEvent unpublished: unsignedEvent
}]); }]);
} else { } else {
setProcessedLessons(prev => [...prev, { setProcessedLessons(prev => [...prev, {
d: lesson?.d, d: lesson?.d,
kind: lesson?.price ? 30402 : 30023, kind: lesson?.price ? 30402 : 30023,
pubkey: lesson.pubkey pubkey: lesson.pubkey,
index: lesson.index
}]); }]);
} }
}); });
@ -323,6 +346,9 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
</div> </div>
<h1 className='text-4xl mt-6'>{processedEvent?.title}</h1> <h1 className='text-4xl mt-6'>{processedEvent?.title}</h1>
<p className='text-xl mt-6'>{processedEvent?.summary}</p> <p className='text-xl mt-6'>{processedEvent?.summary}</p>
{processedEvent?.price && (
<p className='text-lg mt-6'>Price: {processedEvent.price} sats</p>
)}
<div className='flex flex-row w-full mt-6 items-center'> <div className='flex flex-row w-full mt-6 items-center'>
<Image <Image
alt="avatar thumbnail" alt="avatar thumbnail"

View File

@ -76,6 +76,9 @@ const DraftCourseLesson = ({ lesson, course }) => {
</ul> </ul>
</div> </div>
)} )}
{lesson?.price && (
<p className='text-lg mt-6'>Price: {lesson.price} sats</p>
)}
<div className='flex flex-row w-full mt-6 items-center'> <div className='flex flex-row w-full mt-6 items-center'>
<Image <Image
alt="avatar thumbnail" alt="avatar thumbnail"
@ -101,7 +104,7 @@ const DraftCourseLesson = ({ lesson, course }) => {
<div className='flex flex-row w-full mt-6 items-center'> <div className='flex flex-row w-full mt-6 items-center'>
{isPublished ? ( {isPublished ? (
<> <>
<Message severity="success" text="published" /> <Message severity="success" text="published" className="w-auto m-2" />
<Button onClick={() => router.push(`/details/${lesson.id}`)} label="View" outlined className="w-auto m-2" /> <Button onClick={() => router.push(`/details/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
<Button onClick={() => router.push(`/details/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" /> <Button onClick={() => router.push(`/details/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
</> </>

View File

@ -26,7 +26,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => {
} }
const filterContent = (content) => { const filterContent = (content) => {
const contentPrice = content.price || 0; const contentPrice = content.price || content.tags.find(tag => tag[0] === 'price')?.[1] || 0;
return isPaidCourse ? contentPrice > 0 : true; return isPaidCourse ? contentPrice > 0 : true;
}; };

View File

@ -40,22 +40,14 @@ export const createCourse = async (data) => {
data: { data: {
id: data.id, id: data.id,
noteId: data.noteId, noteId: data.noteId,
user: { connect: { id: data.userId } }, price: data.price,
user: { connect: { id: data.user.connect.id } },
lessons: { lessons: {
create: data.lessons.map((lesson, index) => ({ connect: data.lessons.connect
resourceId: lesson.resourceId,
draftId: lesson.draftId,
index: index
}))
} }
}, },
include: { include: {
lessons: { lessons: true,
include: {
resource: true,
draft: true
}
},
user: true user: true
} }
}); });

View File

@ -55,6 +55,7 @@ const DraftCourse = () => {
if (isDraft) { if (isDraft) {
const parsedLessonObject = { const parsedLessonObject = {
...lesson?.draft, ...lesson?.draft,
index: lesson.index,
author: session.user author: session.user
} }
return parsedLessonObject; return parsedLessonObject;
@ -68,6 +69,7 @@ const DraftCourse = () => {
const author = await fetchAuthor(event.pubkey); const author = await fetchAuthor(event.pubkey);
return { return {
...parseEvent(event), ...parseEvent(event),
index: lesson.index,
author author
}; };
} }

View File

@ -106,6 +106,14 @@ const Course = () => {
} }
}, [lessonIds, ndk, fetchAuthor]); }, [lessonIds, ndk, fetchAuthor]);
useEffect(() => {
console.log('lessons', lessons);
}, [lessons]);
useEffect(() => {
console.log('lessonIds', lessonIds);
}, [lessonIds]);
useEffect(() => { useEffect(() => {
if (course?.price && course?.price > 0) { if (course?.price && course?.price > 0) {
setPaidCourse(true); setPaidCourse(true);