mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 01:02:04 +00:00
Allow both drafts and published resources to make it into course draft, seperated selectedContentItem in course form into its own component
This commit is contained in:
parent
054adf6869
commit
d1c121e6e8
@ -91,6 +91,12 @@ CREATE TABLE "CourseDraft" (
|
||||
CONSTRAINT "CourseDraft_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_CourseDraftToDraft" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
|
||||
|
||||
@ -106,6 +112,12 @@ CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CourseDraft_courseId_key" ON "CourseDraft"("courseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_CourseDraftToDraft_AB_unique" ON "_CourseDraftToDraft"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_CourseDraftToDraft_B_index" ON "_CourseDraftToDraft"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@ -138,3 +150,9 @@ ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY (
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_CourseDraftToDraft" ADD CONSTRAINT "_CourseDraftToDraft_A_fkey" FOREIGN KEY ("A") REFERENCES "CourseDraft"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_CourseDraftToDraft" ADD CONSTRAINT "_CourseDraftToDraft_B_fkey" FOREIGN KEY ("B") REFERENCES "Draft"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -82,6 +82,7 @@ model Draft {
|
||||
topics String[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
courseDrafts CourseDraft[]
|
||||
}
|
||||
|
||||
model CourseDraft {
|
||||
@ -89,6 +90,7 @@ model CourseDraft {
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
resources Resource[]
|
||||
drafts Draft[]
|
||||
title String
|
||||
summary String
|
||||
image String?
|
||||
|
@ -76,10 +76,12 @@ const CoursePaymentButton = ({ lnAddress, amount, onSuccess, onError, courseId }
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={`Pay ${amount} sats`}
|
||||
label={`${amount} sats`}
|
||||
onClick={() => setDialogVisible(true)}
|
||||
disabled={!invoice}
|
||||
severity='info'
|
||||
severity='primary'
|
||||
rounded
|
||||
icon='pi pi-wallet'
|
||||
className='text-[#f8f8ff] text-sm'
|
||||
/>
|
||||
<Dialog
|
||||
|
@ -74,11 +74,13 @@ const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resource
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={`Pay ${amount} sats`}
|
||||
label={`${amount} sats`}
|
||||
icon="pi pi-wallet"
|
||||
onClick={() => setDialogVisible(true)}
|
||||
disabled={!invoice}
|
||||
className="p-2 bg-blue-500 text-white rounded"
|
||||
severity='primary'
|
||||
rounded
|
||||
className="text-[#f8f8ff] text-sm"
|
||||
/>
|
||||
<Dialog
|
||||
visible={dialogVisible}
|
||||
|
31
src/components/content/SelectedContentItem.js
Normal file
31
src/components/content/SelectedContentItem.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { formatUnixTimestamp } from "@/utils/time";
|
||||
|
||||
const SelectedContentItem = ({ content }) => {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
return (
|
||||
<div className="w-full border-2 rounded-lg border-gray-700 p-2 rounded-tr-none rounded-br-none">
|
||||
<div className="flex flex-row gap-4">
|
||||
<Image
|
||||
alt="content thumbnail"
|
||||
src={returnImageProxy(content.image)}
|
||||
width={50}
|
||||
height={50}
|
||||
className="w-[100px] h-[100px] object-cover object-center border-round"
|
||||
/>
|
||||
<div className="flex-1 max-w-[80vw]">
|
||||
<div className="text-lg text-900 font-bold">{content.title}</div>
|
||||
<div className="w-full text-sm text-600 text-wrap">{content.summary}</div>
|
||||
<div className="text-sm pt-6 text-gray-500">
|
||||
{content.published_at ? formatUnixTimestamp(content.published_at) : "not yet published"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedContentItem;
|
@ -1,33 +1,11 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { Button } from "primereact/button";
|
||||
import { formatUnixTimestamp } from "@/utils/time";
|
||||
import { Button } from "primereact/button";
|
||||
|
||||
const ContentDropdownItem = ({ content, onSelect, selected }) => {
|
||||
const ContentDropdownItem = ({ content, onSelect }) => {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
console.log('selected:', content);
|
||||
|
||||
if (selected) {
|
||||
return (
|
||||
<div className="w-full border-y-2 border-l-2 rounded-l-lg border-gray-700">
|
||||
<div className="flex flex-row gap-4 p-2">
|
||||
<Image
|
||||
alt="content thumbnail"
|
||||
src={returnImageProxy(content.image)}
|
||||
width={50}
|
||||
height={50}
|
||||
className="w-[100px] h-[100px] object-cover object-center border-round"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-lg text-900 font-bold">{content.title}</div>
|
||||
<div className="min-h-[40px] text-sm text-600 line-clamp-2 break-words">{content.summary}</div>
|
||||
<div className="text-sm pt-6 text-gray-500">{content.published_at ? formatUnixTimestamp(content.published_at) : "not yet published"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full border-t-2 border-gray-700 py-4">
|
||||
@ -42,7 +20,9 @@ const ContentDropdownItem = ({ content, onSelect, selected }) => {
|
||||
<div className="flex-1 max-w-[80vw]">
|
||||
<div className="text-lg text-900 font-bold">{content.title}</div>
|
||||
<div className="w-full text-sm text-600 text-wrap">{content.summary}</div>
|
||||
<div className="text-sm pt-6 text-gray-500">{content.published_at ? formatUnixTimestamp(content.published_at) : "not yet published"}</div>
|
||||
<div className="text-sm pt-6 text-gray-500">
|
||||
{content.published_at ? formatUnixTimestamp(content.published_at) : "not yet published"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-end">
|
||||
<Button label="Select" onClick={() => onSelect(content)} />
|
||||
|
@ -17,6 +17,7 @@ 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';
|
||||
|
||||
|
||||
@ -29,9 +30,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
const [isPaidCourse, setIsPaidCourse] = useState(draft?.price ? true : false);
|
||||
const [price, setPrice] = useState(draft?.price || 0);
|
||||
const [coverImage, setCoverImage] = useState('');
|
||||
const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]);
|
||||
const [loadingLessons, setLoadingLessons] = useState(true);
|
||||
const [selectedLessons, setSelectedLessons] = useState([]);
|
||||
const [selectedContent, setSelectedContent] = useState([]);
|
||||
const [topics, setTopics] = useState(['']);
|
||||
|
||||
const { resources, resourcesLoading, resourcesError } = useResourcesQuery();
|
||||
@ -49,41 +48,6 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('selectedLessons:', selectedLessons);
|
||||
}, [selectedLessons]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLessons = async () => {
|
||||
if (draft && draft?.resources) {
|
||||
const parsedLessons = await Promise.all(
|
||||
draft.resources.map(async (lesson) => {
|
||||
const parsedLesson = await fetchLessonEventFromNostr(lesson.noteId);
|
||||
return parsedLesson;
|
||||
})
|
||||
);
|
||||
setSelectedLessons([...selectedLessons, ...parsedLessons]);
|
||||
setLoadingLessons(false); // Data is loaded
|
||||
} else {
|
||||
setLoadingLessons(false); // No draft means no lessons to load
|
||||
}
|
||||
};
|
||||
|
||||
fetchLessons();
|
||||
}, [draft]); // Only depend on draft
|
||||
|
||||
const fetchLessonEventFromNostr = async (eventId) => {
|
||||
try {
|
||||
await ndk.connect();
|
||||
const fetchedEvent = await ndk.fetchEvent(eventId);
|
||||
if (fetchedEvent) {
|
||||
return parseEvent(fetchedEvent);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('error', 'Error', `Failed to fetch lesson: ${eventId}`);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (draft) {
|
||||
console.log('draft:', draft);
|
||||
@ -92,7 +56,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
setIsPaidCourse(draft.price > 0);
|
||||
setPrice(draft.price || 0);
|
||||
setCoverImage(draft.image);
|
||||
// setSelectedLessons(draft.resources || []);
|
||||
setSelectedContent(draft.resources.concat(draft.drafts) || []);
|
||||
setTopics(draft.topics || ['']);
|
||||
}
|
||||
}, [draft]);
|
||||
@ -110,26 +74,20 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Create the course draft
|
||||
const courseDraftPayload = {
|
||||
userId: user.id, // Make sure this is set
|
||||
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;
|
||||
|
||||
// Step 2: Associate resources with the course draft
|
||||
for (const lesson of selectedLessons) {
|
||||
await axios.put(`/api/resources/${lesson.d}`, {
|
||||
courseDraftId: courseDraftId
|
||||
});
|
||||
}
|
||||
|
||||
showToast('success', 'Success', 'Course draft saved successfully');
|
||||
router.push(`/course/${courseDraftId}/draft`);
|
||||
} catch (error) {
|
||||
@ -138,50 +96,15 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createLessonEvent = (lesson) => {
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = lesson.price ? 30402 : 30023;
|
||||
event.content = lesson.content;
|
||||
event.tags = [
|
||||
['d', lesson.id],
|
||||
['title', lesson.title],
|
||||
['summary', lesson.summary],
|
||||
['image', lesson.image],
|
||||
...lesson.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
];
|
||||
return event;
|
||||
};
|
||||
|
||||
const handleLessonChange = (e, index) => {
|
||||
const selectedLessonId = e.value;
|
||||
const selectedLesson = getContentOptions(index).flatMap(group => group.items).find(lesson => lesson.value === selectedLessonId);
|
||||
|
||||
const updatedLessons = lessons.map((lesson, i) =>
|
||||
i === index ? { ...lesson, id: selectedLessonId, title: selectedLesson.label.props.content.title } : lesson
|
||||
);
|
||||
setLessons(updatedLessons);
|
||||
};
|
||||
|
||||
const handleLessonSelect = (content) => {
|
||||
setSelectedLessons([...selectedLessons, content]);
|
||||
addLesson();
|
||||
};
|
||||
|
||||
const addLesson = () => {
|
||||
setLessons([...lessons, { id: uuidv4(), title: 'Select a lesson' }]);
|
||||
};
|
||||
|
||||
const removeLesson = (index) => {
|
||||
const updatedLessons = lessons.filter((_, i) => i !== index);
|
||||
const updatedSelectedLessons = selectedLessons.filter((_, i) => i !== index);
|
||||
|
||||
if (updatedLessons.length === 0) {
|
||||
updatedLessons.push({ id: uuidv4(), title: 'Select a lesson' });
|
||||
const handleContentSelect = (content) => {
|
||||
if (!selectedContent.some(item => item.id === content.id)) {
|
||||
setSelectedContent([...selectedContent, content]);
|
||||
}
|
||||
};
|
||||
|
||||
setLessons(updatedLessons);
|
||||
setSelectedLessons(updatedSelectedLessons);
|
||||
const removeContent = (index) => {
|
||||
const updatedContent = selectedContent.filter((_, i) => i !== index);
|
||||
setSelectedContent(updatedContent);
|
||||
};
|
||||
|
||||
const addTopic = () => {
|
||||
@ -198,36 +121,34 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
setTopics(updatedTopics);
|
||||
};
|
||||
|
||||
const getContentOptions = (index) => {
|
||||
const getContentOptions = () => {
|
||||
if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filterContent = (content) => {
|
||||
console.log('contentttttt', content);
|
||||
// If there is price in content.tags, then it is a paid content 'price' in the 0 index and stringified int in the 1 index
|
||||
const contentPrice = content.tags.find(tag => tag[0] === 'price') ? parseInt(content.tags.find(tag => tag[0] === 'price')[1]) : 0;
|
||||
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: <ContentDropdownItem content={draft} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />,
|
||||
value: draft.id
|
||||
label: draft.title,
|
||||
value: draft
|
||||
}));
|
||||
|
||||
const resourceOptions = resources.filter(filterContent).map(resource => {
|
||||
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resource);
|
||||
const parsedResource = parseEvent(resource);
|
||||
return {
|
||||
label: <ContentDropdownItem content={{ id, kind, pubkey, content, title, summary, image, published_at, d, topics }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
|
||||
value: id
|
||||
label: parsedResource.title,
|
||||
value: parsedResource
|
||||
};
|
||||
});
|
||||
|
||||
const workshopOptions = workshops.filter(filterContent).map(workshop => {
|
||||
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(workshop);
|
||||
const parsedWorkshop = parseEvent(workshop);
|
||||
return {
|
||||
label: <ContentDropdownItem content={{ id, kind, pubkey, content, title, summary, image, published_at, d, topics }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
|
||||
value: id
|
||||
label: parsedWorkshop.title,
|
||||
value: parsedWorkshop
|
||||
};
|
||||
});
|
||||
|
||||
@ -247,8 +168,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
];
|
||||
};
|
||||
|
||||
// const lessonOptions = getContentOptions();
|
||||
if (loadingLessons || resourcesLoading || workshopsLoading || draftsLoading) {
|
||||
if (resourcesLoading || workshopsLoading || draftsLoading) {
|
||||
return <ProgressSpinner />;
|
||||
}
|
||||
|
||||
@ -279,29 +199,27 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
</div>
|
||||
<div className="mt-8 flex-col w-full">
|
||||
<div className="mt-4 flex-col w-full">
|
||||
{selectedLessons.map((lesson, index) => {
|
||||
return (
|
||||
<div key={lesson.id} className="p-inputgroup flex-1 mt-4">
|
||||
<ContentDropdownItem content={lesson} selected={true} />
|
||||
<Button icon="pi pi-times" className="p-button-danger" onClick={() => removeLesson(index)} />
|
||||
{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>
|
||||
)
|
||||
})
|
||||
}
|
||||
{lessons.map((lesson, index) => (
|
||||
<div key={lesson.id} className="p-inputgroup flex-1 mt-4">
|
||||
))}
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<Dropdown
|
||||
value={lesson.title}
|
||||
options={getContentOptions(index)}
|
||||
onChange={(e) => handleLessonChange(e, index)}
|
||||
placeholder="Select a Lesson"
|
||||
itemTemplate={(option) => option.label}
|
||||
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">
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from "@/db/prisma";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
const { userId, title, summary, image, price, topics, resourceIds } = req.body;
|
||||
const { userId, title, summary, image, price, topics, resources, drafts } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'userId is required' });
|
||||
@ -20,10 +18,13 @@ export default async function handler(req, res) {
|
||||
topics: topics || [],
|
||||
user: { connect: { id: userId } },
|
||||
resources: {
|
||||
connect: resourceIds ? resourceIds.map(id => ({ id })) : []
|
||||
connect: resources ? resources.map(id => ({ id })) : []
|
||||
},
|
||||
drafts: {
|
||||
connect: drafts ? drafts.map(id => ({ id })) : []
|
||||
}
|
||||
},
|
||||
include: { resources: true }
|
||||
include: { resources: true, drafts: true }
|
||||
});
|
||||
|
||||
res.status(201).json(courseDraft);
|
||||
|
Loading…
x
Reference in New Issue
Block a user