Improved draftCourses schema and relations, can publish draft course in new schema, can edit draft course and lessons

This commit is contained in:
austinkelsay 2024-08-16 11:40:50 -05:00
parent 14ead7b35c
commit b8b10907bb
10 changed files with 370 additions and 235 deletions

View File

@ -49,6 +49,7 @@ CREATE TABLE "Resource" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"userId" TEXT NOT NULL, "userId" TEXT NOT NULL,
"courseId" TEXT, "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,
@ -82,18 +83,14 @@ CREATE TABLE "CourseDraft" (
"summary" TEXT NOT NULL, "summary" TEXT NOT NULL,
"image" TEXT, "image" TEXT,
"price" INTEGER DEFAULT 0, "price" INTEGER DEFAULT 0,
"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")
); );
-- CreateTable
CREATE TABLE "_CourseDraftToResource" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey"); 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"); CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "_CourseDraftToResource_AB_unique" ON "_CourseDraftToResource"("A", "B"); CREATE UNIQUE INDEX "CourseDraft_courseId_key" ON "CourseDraft"("courseId");
-- CreateIndex
CREATE INDEX "_CourseDraftToResource_B_index" ON "_CourseDraftToResource"("B");
-- 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;
@ -133,6 +127,9 @@ ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userI
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Resource" ADD CONSTRAINT "Resource_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE; 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;
@ -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; ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "_CourseDraftToResource" ADD CONSTRAINT "_CourseDraftToResource_A_fkey" FOREIGN KEY ("A") REFERENCES "CourseDraft"("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;
-- AddForeignKey
ALTER TABLE "_CourseDraftToResource" ADD CONSTRAINT "_CourseDraftToResource_B_fkey" FOREIGN KEY ("B") REFERENCES "Resource"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -2,7 +2,6 @@ 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"
} }
@ -13,10 +12,10 @@ model User {
username String? @unique username String? @unique
avatar String? avatar String?
purchased Purchase[] purchased Purchase[]
courses Course[] // Relation field added for courses created by the user courses Course[]
resources Resource[] // Relation field added for resources created by the user resources Resource[]
drafts Draft[] // Relation field added for drafts created by the user courseDrafts CourseDraft[]
courseDrafts CourseDraft[] // Relation field added for course drafts created by the user 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())
@ -37,13 +36,13 @@ model Purchase {
courseId String? courseId String?
resource Resource? @relation(fields: [resourceId], references: [id]) resource Resource? @relation(fields: [resourceId], references: [id])
resourceId String? resourceId String?
amountPaid Int // in satoshis amountPaid Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Course { model Course {
id String @id // Client generates UUID 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)
@ -52,6 +51,7 @@ model Course {
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 {
@ -60,9 +60,10 @@ model Resource {
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
course Course? @relation(fields: [courseId], references: [id]) course Course? @relation(fields: [courseId], references: [id])
courseId String? courseId String?
courseDraft CourseDraft? @relation(fields: [courseDraftId], references: [id])
courseDraftId String?
price Int @default(0) price Int @default(0)
purchases Purchase[] purchases Purchase[]
courseDrafts CourseDraft[]
noteId String? @unique noteId String? @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@ -92,6 +93,9 @@ model CourseDraft {
summary String summary String
image String? image String?
price Int? @default(0) price Int? @default(0)
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

@ -54,7 +54,14 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
}, [session]); }, [session]);
const handleDelete = () => { 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) => { const handleSubmit = async (e) => {

View File

@ -19,6 +19,9 @@ import { parseEvent } from "@/utils/nostr";
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem"; import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
import 'primeicons/primeicons.css'; 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 CourseForm = ({ draft = null, isPublished = false }) => {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [summary, setSummary] = useState(''); const [summary, setSummary] = useState('');
@ -96,140 +99,37 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!ndk.signer) { if (!user) {
await addSigner(); showToast('error', 'Error', 'User not authenticated');
return;
} }
// Prepare the lessons from selected lessons try {
const resources = await Promise.all(selectedLessons.map(async (lesson) => { // Step 1: Create the course draft
if (lesson?.type) { const courseDraftPayload = {
const event = createLessonEvent(lesson); userId: user.id, // Make sure this is set
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, title,
summary, summary,
image: coverImage, image: coverImage,
price: price || 0, price: checked ? price : 0,
resources: {
set: resources.map(resource => ({ id: resource.id })),
},
topics, topics,
}; };
try { const courseDraftResponse = await axios.post('/api/courses/drafts', courseDraftPayload);
let response; const courseDraftId = courseDraftResponse.data.id;
if (draft) {
// payload minus topics // Step 2: Associate resources with the course draft
delete payload.topics for (const lesson of selectedLessons) {
response = await axios.put(`/api/courses/drafts/${draft.id}`, payload); await axios.put(`/api/resources/${lesson.d}`, {
showToast('success', 'Success', 'Course draft updated successfully'); courseDraftId: courseDraftId
} else { });
response = await axios.post('/api/courses/drafts', payload);
showToast('success', 'Success', 'Course draft saved successfully');
} }
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) { } catch (error) {
console.error('Error saving course draft:', error); console.error('Error saving course draft:', error);
showToast('error', 'Failed to save course draft. Please try again.'); showToast('error', 'Failed to save course draft', error.response?.data?.details || error.message);
}
};
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.');
} }
}; };
@ -340,7 +240,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
} }
return ( return (
<form onSubmit={isPublished ? handlePublishedCourse : handleSubmit}> <form onSubmit={handleSubmit}>
<div className="p-inputgroup flex-1"> <div className="p-inputgroup flex-1">
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" /> <InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
</div> </div>
@ -398,7 +298,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
<Button type="button" icon="pi pi-plus" onClick={addTopic} className="p-button-outlined mt-2" /> <Button type="button" icon="pi pi-plus" onClick={addTopic} className="p-button-outlined mt-2" />
</div> </div>
<div className="flex justify-center mt-8"> <div className="flex justify-center mt-8">
<Button type="submit" label={draft ? (isPublished ? "Publish" : "Update") : "Submit"} className="p-button-raised p-button-success" /> <Button type="submit" label="Create Draft" className="p-button-raised p-button-success" />
</div> </div>
</form> </form>
); );

View File

@ -0,0 +1,225 @@
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 { useSession } from 'next-auth/react';
import { useRouter } from "next/router";
import { useToast } from "@/hooks/useToast";
import { useNDKContext } from "@/context/NDKContext";
import { useWorkshopsQuery } from "@/hooks/nostrQueries/content/useWorkshopsQuery";
import { useResourcesQuery } from "@/hooks/nostrQueries/content/useResourcesQuery";
import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery";
import { parseEvent } from "@/utils/nostr";
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
import 'primeicons/primeicons.css';
// todo dealing with adding drafts as new lessons
// todo deal with error where 2 new lessons popup when only one is added from the dropdown
// todo on edit lessons need to make sure that the user is still choosing the order those lessons appear in the course
const EditCourseForm = ({ draft }) => {
const [title, setTitle] = useState('');
const [summary, setSummary] = useState('');
const [checked, setChecked] = useState(false);
const [price, setPrice] = useState(0);
const [coverImage, setCoverImage] = useState('');
const [selectedLessons, setSelectedLessons] = useState([]);
const [selectedLessonsLoading, setSelectedLessonsLoading] = useState(false);
const [topics, setTopics] = useState(['']);
const { ndk } = useNDKContext();
const { resources, resourcesLoading } = useResourcesQuery();
const { workshops, workshopsLoading } = useWorkshopsQuery();
const { drafts, draftsLoading } = useDraftsQuery();
const { data: session } = useSession();
const router = useRouter();
const { showToast } = useToast();
useEffect(() => {
if (draft) {
const fetchLessonEventFromNostr = async (eventId) => {
try {
await ndk.connect();
const fetchedEvent = await ndk.fetchEvent(eventId);
return fetchedEvent ? parseEvent(fetchedEvent) : null;
} catch (error) {
showToast('error', 'Error', `Failed to fetch lesson: ${eventId}`);
return null;
}
};
const fetchLessons = async () => {
const fetchedLessons = await Promise.all(
draft.resources.map(lesson => fetchLessonEventFromNostr(lesson.noteId))
);
setSelectedLessons(fetchedLessons.filter(Boolean));
};
fetchLessons();
setTitle(draft.title);
setSummary(draft.summary);
setChecked(draft.price > 0);
setPrice(draft.price || 0);
setCoverImage(draft.image);
setTopics(draft.topics || ['']);
}
}, [draft, ndk, showToast, parseEvent]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Ensure selectedLessons is an array
const lessonsToUpdate = Array.isArray(selectedLessons) ? selectedLessons : [];
// Update newly added lessons with courseDraftId
const updatePromises = lessonsToUpdate
.filter(lesson => lesson && lesson.id && !draft.resources.some(r => r.id === lesson.id))
.map(lesson =>
axios.put(`/api/resources/${lesson.d}`, { courseDraftId: draft.id })
);
await Promise.all(updatePromises);
// Prepare payload for course draft update
const payload = {
id: draft.id, // Include the id in the payload
title,
summary,
image: coverImage,
price: checked ? price : 0,
topics,
resourceIds: lessonsToUpdate.filter(lesson => lesson && lesson.id).map(lesson => lesson.id)
};
// Update course draft
const response = await axios.put(`/api/courses/drafts/${draft.id}`, payload);
console.log('Update response:', response.data);
showToast('success', 'Success', 'Course draft updated successfully');
router.push(`/course/${draft.id}/draft`);
} catch (error) {
console.error('Error updating course draft:', error);
showToast('error', 'Failed to update course draft', error.response?.data?.details || error.message);
}
};
const handleLessonSelect = (content) => {
if (!selectedLessons.some(lesson => lesson.id === content.id)) {
setSelectedLessons(prevLessons => [...prevLessons, content]);
}
};
const removeLesson = (index) => {
const updatedSelectedLessons = selectedLessons.filter((_, i) => i !== index);
setSelectedLessons(updatedSelectedLessons);
};
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 resourceOptions = resources.map(resource => {
const parsedResource = parseEvent(resource);
return {
label: <ContentDropdownItem content={parsedResource} onSelect={handleLessonSelect} selected={selectedLessons.some(lesson => lesson.id === parsedResource.id)} />,
value: parsedResource
};
});
const workshopOptions = workshops.map(workshop => {
const parsedWorkshop = parseEvent(workshop);
return {
label: <ContentDropdownItem content={parsedWorkshop} onSelect={handleLessonSelect} selected={selectedLessons.some(lesson => lesson.id === parsedWorkshop.id)} />,
value: parsedWorkshop
};
});
return [
{ label: 'Resources', items: resourceOptions },
{ label: 'Workshops', items: workshopOptions }
];
};
if (resourcesLoading || workshopsLoading || draftsLoading || selectedLessonsLoading) {
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-4 flex-col">
<p className="py-2">Paid Course</p>
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} />
{checked && (
<div className="p-inputgroup flex-1 py-4">
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
</div>
)}
</div>
<div className="mt-8 flex-col w-full">
<div className="mt-4 flex-col w-full">
{selectedLessons.map((lesson, index) => (
<div key={index} className="p-inputgroup flex-1 mt-4">
<ContentDropdownItem content={lesson} selected={true} />
<Button icon="pi pi-times" className="p-button-danger" onClick={() => removeLesson(index)} />
</div>
))}
<div className="p-inputgroup flex-1 mt-4">
<Dropdown
options={getContentOptions()}
onChange={(e) => handleLessonSelect(e.value)}
placeholder="Add a Lesson"
optionLabel="label"
optionGroupLabel="label"
optionGroupChildren="items"
value={null}
/>
</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="Update Draft" className="p-button-raised p-button-success" />
</div>
</form>
);
}
export default EditCourseForm;

View File

@ -39,14 +39,16 @@ export const createCourseDraft = async (data) => {
// 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, ...otherData } = data;
return await prisma.courseDraft.update({ return await prisma.courseDraft.update({
where: { id }, where: { id },
data: { data: {
...data, ...otherData,
resources: { resources: {
set: data.resourceIds?.map((resourceId) => ({ id: resourceId })), set: resourceIds?.map((resourceId) => ({ id: resourceId })),
}, },
}, },
include: { resources: true }
}); });
}; };

View File

@ -1,10 +1,9 @@
import { getAllCourseDraftsByUserId, getCourseDraftById, updateCourseDraft, deleteCourseDraft } from "@/db/models/courseDraftModels"; import { getAllCourseDraftsByUserId, getCourseDraftById, updateCourseDraft, deleteCourseDraft } from "@/db/models/courseDraftModels";
import prisma from "@/db/prisma";
export default async function handler(req, res) { export default async function handler(req, res) {
const { slug } = req.query; const { slug } = req.query;
console.log('slug:', slug);
const userId = req.body?.userId || req.query?.userId; const userId = req.body?.userId || req.query?.userId;
console.log('userId:', userId);
if (req.method === 'GET') { if (req.method === 'GET') {
if (slug && !userId) { if (slug && !userId) {
@ -29,18 +28,29 @@ export default async function handler(req, res) {
res.status(400).json({ error: 'User ID is required' }); res.status(400).json({ error: 'User ID is required' });
} }
} else if (req.method === 'PUT') { } else if (req.method === 'PUT') {
if (!slug) {
return res.status(400).json({ error: 'Slug is required to update a course draft' });
}
try { try {
const updatedCourseDraft = await updateCourseDraft(slug, req.body); const { slug } = req.query;
const { title, summary, image, price, topics } = req.body;
const updatedCourseDraft = await prisma.courseDraft.update({
where: { id: slug },
data: {
title,
summary,
image,
price,
topics,
},
});
res.status(200).json(updatedCourseDraft); res.status(200).json(updatedCourseDraft);
} catch (error) { } catch (error) {
res.status(400).json({ error: error.message }); console.error('Error updating course draft:', error);
res.status(500).json({ error: 'Failed to update course draft' });
} }
} else if (req.method === 'DELETE') { } else if (req.method === 'DELETE') {
if (!slug) { if (!slug) {
return res.status(400).json({ error: 'Slug is required to delete a course draft' }); return res.status(400).json({ error: 'Id is required to delete a course draft' });
} }
try { try {
await deleteCourseDraft(slug); await deleteCourseDraft(slug);

View File

@ -1,18 +1,38 @@
import { createCourseDraft } from "@/db/models/courseDraftModels"; import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.method === 'POST') { if (req.method === 'POST') {
try { try {
if (!req.body || !req.body.userId) { const { userId, title, summary, image, price, topics, resourceIds } = req.body;
return res.status(400).json({ error: 'User ID is required' });
if (!userId) {
return res.status(400).json({ error: 'userId is required' });
} }
const newCourseDraft = await createCourseDraft(req.body); const courseDraft = await prisma.courseDraft.create({
res.status(201).json(newCourseDraft); data: {
title,
summary,
image,
price,
topics: topics || [],
user: { connect: { id: userId } },
resources: {
connect: resourceIds ? resourceIds.map(id => ({ id })) : []
}
},
include: { resources: true }
});
res.status(201).json(courseDraft);
} catch (error) { } catch (error) {
res.status(400).json({ error: error.message }); console.error('Error creating course draft:', error);
res.status(500).json({ error: 'Failed to create course draft', details: error.message });
} }
} else { } else {
res.status(405).json({ error: 'Method not allowed' }); res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
} }
} }

View File

@ -16,22 +16,14 @@ export default async function handler(req, res) {
} }
} else if (req.method === 'PUT') { } else if (req.method === 'PUT') {
try { try {
// Fetch the resource by ID to check if it's part of a course console.log('req.body:', req.body);
console.log('slug:', slug);
const resource = await getResourceById(slug); const resource = await getResourceById(slug);
if (!resource) { if (!resource) {
return res.status(404).json({ error: 'Resource not found' }); return res.status(404).json({ error: 'Resource not found' });
} }
// Check if the resource is part of a course
const isPartOfAnyCourse = resource.courseId !== null;
if (isPartOfAnyCourse) {
// Update the specific lesson in the course
await updateLessonInCourse(resource.courseId, slug, req.body);
}
// Update the resource
const updatedResource = await updateResource(slug, req.body); const updatedResource = await updateResource(slug, req.body);
res.status(200).json(updatedResource); res.status(200).json(updatedResource);

View File

@ -1,35 +1,22 @@
import React, { useState, useEffect, use } from "react"; import React, { useState, useEffect } from "react";
import axios from "axios"; import axios from "axios";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import CourseForm from "@/components/forms/CourseForm"; import EditCourseForm from "@/components/forms/EditCourseForm";
import { useNDKContext } from "@/context/NDKContext"; import { useNDKContext } from "@/context/NDKContext";
import { useToast } from "@/hooks/useToast"; import { useToast } from "@/hooks/useToast";
export default function Edit() { export default function Edit() {
const [event, setEvent] = useState(null);
const [draft, setDraft] = useState(null); const [draft, setDraft] = useState(null);
const { ndk, addSigner } = useNDKContext(); const { ndk } = useNDKContext();
const router = useRouter(); const router = useRouter();
const { showToast } = useToast(); const { showToast } = useToast();
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
const { slug } = router.query; const { slug } = router.query;
const fetchEvent = async () => { const fetchDraft = async () => {
// await ndk.connect();
// const fetchedEvent = await ndk.fetchEvent(slug);
// if (fetchedEvent) {
// const parsedEvent = parseEvent(fetchedEvent);
// console.log('parsedEvent:', parsedEvent);
// setEvent(parsedEvent);
// } else {
// If no event found, try to fetch draft
try { try {
console.log('fetching draft:', slug);
const response = await axios.get(`/api/courses/drafts/${slug}`); const response = await axios.get(`/api/courses/drafts/${slug}`);
console.log('response:', response);
if (response.status === 200) { if (response.status === 200) {
setDraft(response.data); setDraft(response.data);
} else { } else {
@ -39,21 +26,15 @@ export default function Edit() {
console.error('Error fetching draft:', error); console.error('Error fetching draft:', error);
showToast('error', 'Error', 'Failed to fetch draft.'); showToast('error', 'Error', 'Failed to fetch draft.');
} }
// }
} }
fetchEvent(); fetchDraft();
} }
}, [router.isReady, router.query, ndk, showToast]); }, [router.isReady, router.query, showToast]);
return ( return (
<div className="w-[80vw] max-w-[80vw] mx-auto my-8 flex flex-col justify-center"> <div className="w-[80vw] max-w-[80vw] mx-auto my-8 flex flex-col justify-center">
<h2 className="text-center mb-8">Edit {event ? 'Published Event' : 'Draft'}</h2> <h2 className="text-center mb-8">Edit Course Draft</h2>
{draft && {draft && <EditCourseForm draft={draft} />}
<CourseForm
draft={draft}
// isPublished
/>
}
</div> </div>
); );
} }