diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js index 20345bf..4ef9d5e 100644 --- a/src/components/content/carousels/templates/CourseTemplate.js +++ b/src/components/content/carousels/templates/CourseTemplate.js @@ -5,6 +5,7 @@ import { formatTimestampToHowLongAgo } from "@/utils/time"; import { useImageProxy } from "@/hooks/useImageProxy"; import { getTotalFromZaps } from "@/utils/lightning"; import ZapDisplay from "@/components/zaps/ZapDisplay"; +import { Tag } from "primereact/tag"; import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; const CourseTemplate = ({ course }) => { @@ -47,6 +48,11 @@ const CourseTemplate = ({ course }) => { {course.name || course.title}

{course.description || course.summary}

+ {course.price && course.price > 0 ? ( +

Price: {course.price} sats

+ ) : ( +

Free

+ )}

{course?.published_at && course.published_at !== "" ? ( @@ -57,6 +63,13 @@ const CourseTemplate = ({ course }) => {

+ {course?.topics && course?.topics.length > 0 && ( +
+ {course.topics.map((topic, index) => ( + + ))} +
+ )} ); diff --git a/src/components/content/carousels/templates/ResourceTemplate.js b/src/components/content/carousels/templates/ResourceTemplate.js index 8d8a668..9babfbb 100644 --- a/src/components/content/carousels/templates/ResourceTemplate.js +++ b/src/components/content/carousels/templates/ResourceTemplate.js @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import { formatTimestampToHowLongAgo } from "@/utils/time"; import { useImageProxy } from "@/hooks/useImageProxy"; import { getTotalFromZaps } from "@/utils/lightning"; +import { Tag } from "primereact/tag"; import ZapDisplay from "@/components/zaps/ZapDisplay"; import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; @@ -47,13 +48,25 @@ const ResourceTemplate = ({ resource }) => {

{resource.title}

-

{resource.summary}

+

{resource.summary}

+ {resource.price && resource.price > 0 ? ( +

Price: {resource.price} sats

+ ) : ( +

Free

+ )}

{formatTimestampToHowLongAgo(resource.published_at)}

+ {resource?.topics && resource?.topics.length > 0 && ( +
+ {resource.topics.map((topic, index) => ( + + ))} +
+ )} ); diff --git a/src/components/content/carousels/templates/WorkshopTemplate.js b/src/components/content/carousels/templates/WorkshopTemplate.js index 12ee82b..5a133a1 100644 --- a/src/components/content/carousels/templates/WorkshopTemplate.js +++ b/src/components/content/carousels/templates/WorkshopTemplate.js @@ -6,6 +6,7 @@ import { useImageProxy } from "@/hooks/useImageProxy"; import { getTotalFromZaps } from "@/utils/lightning"; import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription"; import ZapDisplay from "@/components/zaps/ZapDisplay"; +import { Tag } from "primereact/tag"; const WorkshopTemplate = ({ workshop }) => { const [zapAmount, setZapAmount] = useState(null); @@ -45,12 +46,24 @@ const WorkshopTemplate = ({ workshop }) => { {workshop.title}

{workshop.summary}

+ {workshop.price && workshop.price > 0 ? ( +

Price: {workshop.price} sats

+ ) : ( +

Free

+ )}

{formatTimestampToHowLongAgo(workshop.published_at)}

+ {workshop?.topics && workshop?.topics.length > 0 && ( +
+ {workshop.topics.map((topic, index) => ( + + ))} +
+ )} ); diff --git a/src/components/content/lists/ContentListItem.js b/src/components/content/lists/ContentListItem.js index c4398dc..27a99e4 100644 --- a/src/components/content/lists/ContentListItem.js +++ b/src/components/content/lists/ContentListItem.js @@ -3,6 +3,8 @@ import Image from "next/image"; import { Button } from "primereact/button"; import { useImageProxy } from "@/hooks/useImageProxy"; import { useRouter } from "next/router"; +import { Divider } from 'primereact/divider'; + const ContentListItem = (content) => { const { returnImageProxy } = useImageProxy(); @@ -50,6 +52,7 @@ const ContentListItem = (content) => { + ); }; diff --git a/src/components/forms/course/CourseForm.js b/src/components/forms/course/CourseForm.js index 225a8c0..129d281 100644 --- a/src/components/forms/course/CourseForm.js +++ b/src/components/forms/course/CourseForm.js @@ -60,7 +60,7 @@ const CourseForm = ({ draft = null }) => { try { // First, create the courseDraft const courseDraftData = { - userId: session.user.id, + user: session.user.id, title, summary, image: coverImage, @@ -124,6 +124,34 @@ const CourseForm = ({ draft = null }) => { setTopics(updatedTopics); }; + const handleNewResourceCreate = async (newResource) => { + try { + const response = await axios.post('/api/drafts', newResource); + const createdResource = response.data; + setAllContent(prevContent => [...prevContent, createdResource]); + return createdResource; + } catch (error) { + console.error('Error creating resource draft:', error); + showToast('error', 'Error', 'Failed to create resource draft'); + return null; + } + }; + + const handleNewWorkshopCreate = async (newWorkshop) => { + try { + console.log('newWorkshop', newWorkshop); + const response = await axios.post('/api/drafts', newWorkshop); + console.log('response', response); + const createdWorkshop = response.data; + setAllContent(prevContent => [...prevContent, createdWorkshop]); + return createdWorkshop; + } catch (error) { + console.error('Error creating workshop draft:', error); + showToast('error', 'Error', 'Failed to create workshop draft'); + return null; + } + }; + if (resourcesLoading || workshopsLoading || draftsLoading) { return ; } @@ -158,6 +186,8 @@ const CourseForm = ({ draft = null }) => { lessons={lessons} setLessons={setLessons} allContent={allContent} + onNewResourceCreate={handleNewResourceCreate} + onNewWorkshopCreate={handleNewWorkshopCreate} />
{topics.map((topic, index) => ( diff --git a/src/components/forms/course/LessonSelector.js b/src/components/forms/course/LessonSelector.js index a6fadde..4f915e2 100644 --- a/src/components/forms/course/LessonSelector.js +++ b/src/components/forms/course/LessonSelector.js @@ -3,13 +3,13 @@ import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; import { Dialog } from 'primereact/dialog'; import { Accordion, AccordionTab } from 'primereact/accordion'; -import ResourceForm from '../ResourceForm'; -import WorkshopForm from '../WorkshopForm'; +import EmbeddedResourceForm from '@/components/forms/course/embedded/EmbeddedResourceForm'; +import EmbeddedWorkshopForm from '@/components/forms/course/embedded/EmbeddedWorkshopform'; import ContentDropdownItem from '@/components/content/dropdowns/ContentDropdownItem'; import SelectedContentItem from '@/components/content/SelectedContentItem'; import { parseEvent } from '@/utils/nostr'; -const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => { +const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewResourceCreate, onNewWorkshopCreate }) => { const [showResourceForm, setShowResourceForm] = useState(false); const [showWorkshopForm, setShowWorkshopForm] = useState(false); const [contentOptions, setContentOptions] = useState([]); @@ -114,14 +114,21 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => { setLessons([...lessons, { index: lessons.length }]); }; - const handleNewResourceSave = (newResource) => { - setLessons([...lessons, { ...newResource, index: lessons.length }]); - setShowResourceForm(false); + const handleNewResourceSave = async (newResource) => { + const createdResource = await onNewResourceCreate(newResource); + if (createdResource) { + handleContentSelect(createdResource, lessons.length); + setShowResourceForm(false); + } }; - const handleNewWorkshopSave = (newWorkshop) => { - setLessons([...lessons, { ...newWorkshop, index: lessons.length }]); - setShowWorkshopForm(false); + const handleNewWorkshopSave = async (newWorkshop) => { + console.log('newWorkshop', newWorkshop); + const createdWorkshop = await onNewWorkshopCreate(newWorkshop); + if (createdWorkshop) { + handleContentSelect(createdWorkshop, lessons.length); + setShowWorkshopForm(false); + } }; const handleTabChange = (e) => { @@ -158,8 +165,8 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => {
{lesson.id ? null : ( <> -
@@ -181,12 +188,12 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => { type="button" // Explicitly set type to "button" /> - setShowResourceForm(false)} header="Create New Resource"> - + setShowResourceForm(false)} header="Create New Resource"> + - setShowWorkshopForm(false)} header="Create New Workshop"> - + setShowWorkshopForm(false)} header="Create New Workshop"> +
); diff --git a/src/components/forms/course/embedded/EmbeddedResourceForm.js b/src/components/forms/course/embedded/EmbeddedResourceForm.js new file mode 100644 index 0000000..d348c90 --- /dev/null +++ b/src/components/forms/course/embedded/EmbeddedResourceForm.js @@ -0,0 +1,226 @@ +import React, { useState, useEffect, useCallback } 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 { useSession } from "next-auth/react"; +import { useToast } from "@/hooks/useToast"; +import { useNDKContext } from "@/context/NDKContext"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import dynamic from 'next/dynamic'; +const MDEditor = dynamic( + () => import("@uiw/react-md-editor"), + { + ssr: false, + } +); +import 'primeicons/primeicons.css'; +import { Tooltip } from 'primereact/tooltip'; +import 'primereact/resources/primereact.min.css'; + +const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPaid }) => { + const [title, setTitle] = useState(draft?.title || ''); + const [summary, setSummary] = useState(draft?.summary || ''); + const [isPaidResource, setIsPaidResource] = useState(isPaid); + const [price, setPrice] = useState(draft?.price || 0); + const [coverImage, setCoverImage] = useState(draft?.image || ''); + const [topics, setTopics] = useState(draft?.topics || ['']); + const [content, setContent] = useState(draft?.content || ''); + const [user, setUser] = useState(null); + const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']); + + const { data: session, status } = useSession(); + const { showToast } = useToast(); + const { ndk, addSigner } = useNDKContext(); + + useEffect(() => { + console.log('isPublished', isPublished); + console.log('draft', draft); + }, [isPublished, draft]); + + useEffect(() => { + if (session) { + console.log('session', session.user); + setUser(session.user); + } + }, [session]); + + const handleContentChange = useCallback((value) => { + setContent(value || ''); + }, []); + + useEffect(() => { + if (draft) { + setTitle(draft.title); + setSummary(draft.summary); + setIsPaidResource(draft.price ? true : false); + setPrice(draft.price || 0); + setContent(draft.content); + setCoverImage(draft.image); + setTopics(draft.topics || []); + setAdditionalLinks(draft.additionalLinks || []); + } + }, [draft]); + + const buildEvent = async (draft) => { + const dTag = draft.d + const event = new NDKEvent(ndk); + let encryptedContent; + + if (draft?.price) { + // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY + encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content); + } + + event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present + event.content = draft?.price ? encryptedContent : draft.content; + event.created_at = Math.floor(Date.now() / 1000); + event.pubkey = user.pubkey; + event.tags = [ + ['d', dTag], + ['title', draft.title], + ['summary', draft.summary], + ['image', draft.image], + ...draft.topics.map(topic => ['t', topic]), + ['published_at', Math.floor(Date.now() / 1000).toString()], + ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), + ]; + + return event; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + const payload = { + title, + summary, + type: 'resource', + price: isPaidResource ? price : null, + content, + image: coverImage, + topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'resource'])], + additionalLinks: additionalLinks.filter(link => link.trim() !== ''), + user: user?.id || user?.pubkey + }; + + if (onSave) { + try { + await onSave(payload); + showToast('success', 'Success', draft ? 'Resource updated successfully.' : 'Resource created successfully.'); + } catch (error) { + console.error(error); + showToast('error', 'Error', 'Failed to save resource. Please try again.'); + } + } + }; + + const handleTopicChange = (index, value) => { + const updatedTopics = topics.map((topic, i) => i === index ? value : topic); + setTopics(updatedTopics); + }; + + const addTopic = (e) => { + e.preventDefault(); + setTopics([...topics, '']); // Add an empty string to the topics array + }; + + const removeTopic = (e, index) => { + e.preventDefault(); + const updatedTopics = topics.filter((_, i) => i !== index); + setTopics(updatedTopics); + }; + + const handleAdditionalLinkChange = (index, value) => { + const updatedAdditionalLinks = additionalLinks.map((link, i) => i === index ? value : link); + setAdditionalLinks(updatedAdditionalLinks); + }; + + const addAdditionalLink = (e) => { + e.preventDefault(); + setAdditionalLinks([...additionalLinks, '']); // Add an empty string to the additionalLinks array + }; + + const removeAdditionalLink = (e, index) => { + e.preventDefault(); + const updatedAdditionalLinks = additionalLinks.filter((_, i) => i !== index); + setAdditionalLinks(updatedAdditionalLinks); + }; + + return ( +
+
+ setTitle(e.target.value)} placeholder="Title" /> +
+
+ setSummary(e.target.value)} placeholder="Summary" /> +
+
+ setCoverImage(e.target.value)} placeholder="Cover Image URL" /> +
+ +
+

Paid Resource

+ setIsPaidResource(e.value)} /> + {isPaidResource && ( +
+ setPrice(e.value)} placeholder="Price (sats)" /> +
+ )} +
+
+ Content +
+ +
+
+
+ + External Links + + + {additionalLinks.map((link, index) => ( +
+ handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" /> + {index > 0 && ( +
+ ))} +
+
+ +
+
+ {topics.map((topic, index) => ( +
+ handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" /> + {index > 0 && ( +
+ ))} +
+
+
+
+
+ + ); +} + +export default EmbeddedResourceForm; \ No newline at end of file diff --git a/src/components/forms/course/embedded/EmbeddedWorkshopForm.js b/src/components/forms/course/embedded/EmbeddedWorkshopForm.js new file mode 100644 index 0000000..c08603b --- /dev/null +++ b/src/components/forms/course/embedded/EmbeddedWorkshopForm.js @@ -0,0 +1,188 @@ +import React, { useState, useEffect } 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 { useToast } from '@/hooks/useToast'; +import { useSession } from 'next-auth/react'; +import 'primeicons/primeicons.css'; +import { Tooltip } from 'primereact/tooltip'; +import 'primereact/resources/primereact.min.css'; + +const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => { + const [title, setTitle] = useState(draft?.title || ''); + const [summary, setSummary] = useState(draft?.summary || ''); + const [price, setPrice] = useState(draft?.price || 0); + const [isPaidResource, setIsPaidResource] = useState(isPaid); + const [videoUrl, setVideoUrl] = useState(draft?.content || ''); + const [coverImage, setCoverImage] = useState(draft?.image || ''); + const [topics, setTopics] = useState(draft?.topics || ['']); + const [user, setUser] = useState(); + const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']); + + const { showToast } = useToast(); + const { data: session, status } = useSession(); + + useEffect(() => { + if (session) { + console.log('session', session.user); + setUser(session.user); + } + }, [session]); + + useEffect(() => { + if (draft) { + setTitle(draft.title); + setSummary(draft.summary); + setPrice(draft.price || 0); + setIsPaidResource(draft.price ? true : false); + setVideoUrl(draft.content); + setCoverImage(draft.image); + setTopics(draft.topics || ['']); + setAdditionalLinks(draft.additionalLinks || ['']); + } + }, [draft]); + + const handleSubmit = async (e) => { + e.preventDefault(); + let embedCode = ''; + + // Check if it's a YouTube video + if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { + const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop(); + embedCode = `
`; + } + // Check if it's a Vimeo video + else if (videoUrl.includes('vimeo.com')) { + const videoId = videoUrl.split('/').pop(); + embedCode = `
`; + } + // Add more conditions here for other video services + + const payload = { + title, + summary, + type: 'workshop', + price: isPaidResource ? price : null, + content: embedCode, + image: coverImage, + topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'workshop'])], + additionalLinks: additionalLinks.filter(link => link.trim() !== ''), + user: user?.id || user?.pubkey + }; + + if (onSave) { + try { + await onSave(payload); + showToast('success', 'Success', draft ? 'Workshop updated successfully.' : 'Workshop created successfully.'); + } catch (error) { + console.error(error); + showToast('error', 'Error', 'Failed to save workshop. Please try again.'); + } + } + }; + + const handleTopicChange = (index, value) => { + const updatedTopics = topics.map((topic, i) => i === index ? value : topic); + setTopics(updatedTopics); + }; + + const addTopic = (e) => { + e.preventDefault(); + setTopics([...topics, '']); // Add an empty string to the topics array + }; + + const removeTopic = (e, index) => { + e.preventDefault(); + const updatedTopics = topics.filter((_, i) => i !== index); + setTopics(updatedTopics); + }; + + const handleLinkChange = (index, value) => { + const updatedLinks = additionalLinks.map((link, i) => i === index ? value : link); + setAdditionalLinks(updatedLinks); + }; + + const addLink = (e) => { + e.preventDefault(); + setAdditionalLinks([...additionalLinks, '']); + }; + + const removeLink = (e, index) => { + e.preventDefault(); + const updatedLinks = additionalLinks.filter((_, i) => i !== index); + setAdditionalLinks(updatedLinks); + }; + + + return ( +
+
+ setTitle(e.target.value)} placeholder="Title" /> +
+
+ setSummary(e.target.value)} placeholder="Summary" /> +
+ +
+

Paid Workshop

+ setIsPaidResource(e.value)} /> + {isPaidResource && ( +
+ + setPrice(e.value)} placeholder="Price (sats)" /> +
+ )} +
+
+ setVideoUrl(e.target.value)} placeholder="Video URL" /> +
+
+ setCoverImage(e.target.value)} placeholder="Cover Image URL" /> +
+
+ + External Links + + + {additionalLinks.map((link, index) => ( +
+ handleLinkChange(index, e.target.value)} placeholder="https://example.com" className="w-full mt-2" /> + {index > 0 && ( +
+ ))} +
+
+ +
+
+ {topics.map((topic, index) => ( +
+ handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" /> + {index > 0 && ( +
+ ))} +
+
+
+
+
+ + ); +} + +export default EmbeddedWorkshopForm; \ No newline at end of file diff --git a/src/db/models/courseDraftModels.js b/src/db/models/courseDraftModels.js index af21ab2..c6ff306 100644 --- a/src/db/models/courseDraftModels.js +++ b/src/db/models/courseDraftModels.js @@ -43,7 +43,7 @@ export const createCourseDraft = async (data) => { return await prisma.courseDraft.create({ data: { ...data, - user: { connect: { id: data.userId } }, + user: { connect: { id: data.user } }, }, include: { draftLessons: { diff --git a/src/pages/api/courses/drafts/index.js b/src/pages/api/courses/drafts/index.js index c7d8e28..d18b0e1 100644 --- a/src/pages/api/courses/drafts/index.js +++ b/src/pages/api/courses/drafts/index.js @@ -3,25 +3,7 @@ import { createCourseDraft } from "@/db/models/courseDraftModels"; export default async function handler(req, res) { if (req.method === 'POST') { try { - const { userId, title, summary, image, price, topics, draftLessons } = req.body; - - if (!userId) { - return res.status(400).json({ error: 'userId is required' }); - } - - const courseDraft = await createCourseDraft({ - userId, - title, - summary, - image, - price, - topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase())])], - draftLessons: draftLessons?.map((lesson, index) => ({ - draftId: lesson.draftId, - resourceId: lesson.resourceId, - index - })) || [] - }); + const courseDraft = await createCourseDraft(req.body); res.status(201).json(courseDraft); } catch (error) {