From 7ce97d158c90776640564d0548c0084f8687e4ff Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 29 Apr 2024 14:48:15 -0500 Subject: [PATCH] Continuing to tie together the drafts forms and course form and working on paid content --- src/components/forms/CourseForm.js | 23 ++-- src/components/forms/WorkshopForm.js | 196 +++++---------------------- src/hooks/useNostr.js | 34 +++-- src/pages/draft/[slug].js | 58 ++++++-- 4 files changed, 112 insertions(+), 199 deletions(-) diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 69be9bf..f134f3e 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -85,7 +85,7 @@ const CourseForm = () => { ['title', lesson.title], ['summary', lesson.summary], ['image', lesson.image], - ['t', ...lesson.topics], + ...lesson.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ['price', lesson.price], ['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`], @@ -101,7 +101,7 @@ const CourseForm = () => { ['title', lesson.title], ['summary', lesson.summary], ['image', lesson.image], - ['t', ...lesson.topics], + ...lesson.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()] ] }; @@ -134,9 +134,10 @@ const CourseForm = () => { // // Parse the fields from the lessons to get all of the necessary information const parsedLessons = fetchedLessons.map((lesson) => { - const { id, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(lesson); + const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(lesson); return { id, + kind, pubkey, content, title, @@ -153,26 +154,24 @@ const CourseForm = () => { const courseEvent = { kind: 30005, created_at: Math.floor(Date.now() / 1000), - content: JSON.stringify({ - title, - summary, - price, - topics, - }), + content: "", tags: [ ['d', uuidv4()], ['name', title], ['picture', coverImage], ['about', summary], - parsedLessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]), + ...parsedLessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]), ], }; console.log('courseEvent:', courseEvent); + + // Sign the course event + const signedCourseEvent = await window?.nostr?.signEvent(courseEvent); + // Publish the course event using Nostr + // await publish(signedCourseEvent); } - // Publish the course event using Nostr - // await publishCourse(courseEvent); // Reset the form fields after publishing the course setTitle(''); diff --git a/src/components/forms/WorkshopForm.js b/src/components/forms/WorkshopForm.js index 3d55969..bc144cf 100644 --- a/src/components/forms/WorkshopForm.js +++ b/src/components/forms/WorkshopForm.js @@ -1,13 +1,10 @@ import React, { useState } from 'react'; import axios from 'axios'; +import router from 'next/router'; import { InputText } from 'primereact/inputtext'; import { InputNumber } from 'primereact/inputnumber'; import { InputSwitch } from 'primereact/inputswitch'; -import { FileUpload } from 'primereact/fileupload'; -import { verifyEvent, nip19 } from "nostr-tools" -import { useNostr } from '@/hooks/useNostr'; import { Button } from 'primereact/button'; -import { v4 as uuidv4 } from 'uuid'; import { useToast } from '@/hooks/useToast'; import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; import 'primeicons/primeicons.css'; @@ -15,19 +12,19 @@ import 'primeicons/primeicons.css'; const WorkshopForm = () => { const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); - const [checked, setChecked] = useState(false); const [price, setPrice] = useState(0); + const [isPaidResource, setIsPaidResource] = useState(false); const [videoUrl, setVideoUrl] = useState(''); const [coverImage, setCoverImage] = useState(''); const [topics, setTopics] = useState(['']); + const router = useRouter(); + const [user] = useLocalStorageWithEffect('user', {}); const { showToast } = useToast(); - const { publishAll } = useNostr(); - - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); let embedCode = ''; @@ -42,169 +39,42 @@ const WorkshopForm = () => { embedCode = ``; } // Add more conditions here for other video services + + const userResponse = await axios.get(`/api/users/${user.pubkey}`); + + if (!userResponse.data) { + showToast('error', 'Error', 'User not found', 'Please try again.'); + return; + } const payload = { title, summary, - isPaidResource: checked, - price: checked ? price : null, - embedCode, + type: 'workshop', + price: isPaidResource ? price : null, + content: embedCode, + image: coverImage, + user: userResponse.data.id, topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'workshop'] }; - console.log(payload); - if (checked) { - broadcastPaidWorkshop(payload); - } else { - broadcastFreeWorkshop(payload); + if (payload && payload.user) { + axios.post('/api/drafts', payload) + .then(response => { + if (response.status === 201) { + showToast('success', 'Success', 'Workshop saved as draft.'); + + if (response.data?.id) { + router.push(`/draft/${response.data.id}`); + } + } + }) + .catch(error => { + console.error(error); + }); } }; - const broadcastFreeWorkshop = async (payload) => { - const newWorkshopId = uuidv4(); - const event = { - kind: 30023, - content: payload.embedCode, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['d', newWorkshopId], - ['title', payload.title], - ['summary', payload.summary], - ['image', ''], - ['t', ...topics], - ['published_at', Math.floor(Date.now() / 1000).toString()], - ] - }; - - const signedEvent = await window.nostr.signEvent(event); - - const eventVerification = await verifyEvent(signedEvent); - - if (!eventVerification) { - showToast('error', 'Error', 'Event verification failed. Please try again.'); - return; - } - - const nAddress = nip19.naddrEncode({ - pubkey: signedEvent.pubkey, - kind: signedEvent.kind, - identifier: newWorkshopId, - }) - - console.log('nAddress:', nAddress); - - const userResponse = await axios.get(`/api/users/${user.pubkey}`) - - if (!userResponse.data) { - showToast('error', 'Error', 'User not found', 'Please try again.'); - return; - } - - const resourcePayload = { - id: newWorkshopId, - userId: userResponse.data.id, - price: 0, - noteId: nAddress, - } - const response = await axios.post(`/api/resources`, resourcePayload); - - console.log('response:', response); - - if (response.status !== 201) { - showToast('error', 'Error', 'Failed to create resource. Please try again.'); - return; - } - - const publishResponse = await publishAll(signedEvent); - - if (!publishResponse) { - showToast('error', 'Error', 'Failed to publish resource. Please try again.'); - return; - } else if (publishResponse?.failedRelays) { - publishResponse?.failedRelays.map(relay => { - showToast('warn', 'Warning', `Failed to publish to relay: ${relay}`); - }); - } - - publishResponse?.successfulRelays.map(relay => { - showToast('success', 'Success', `Published to relay: ${relay}`); - }) - } - - // For images, whether included in the markdown content or not, clients SHOULD use image tags as described in NIP-58. This allows clients to display images in carousel format more easily. - const broadcastPaidWorkshop = async (payload) => { - // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY - const encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY ,process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, payload.content); - const newWorkshopId = uuidv4(); - const event = { - kind: 30402, - content: encryptedContent, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['title', payload.title], - ['summary', payload.summary], - ['t', ...topics], - ['image', ''], - ['d', newresourceId], - ['location', `https://plebdevs.com/resource/${newWorkshopId}`], - ['published_at', Math.floor(Date.now() / 1000).toString()], - ['price', payload.price] - ] - }; - - const signedEvent = await window.nostr.signEvent(event); - - const eventVerification = await verifyEvent(signedEvent); - - if (!eventVerification) { - showToast('error', 'Error', 'Event verification failed. Please try again.'); - return; - } - - const nAddress = nip19.naddrEncode({ - pubkey: signedEvent.pubkey, - kind: signedEvent.kind, - identifier: newWorkshopId, - }) - - console.log('nAddress:', nAddress); - - const userResponse = await axios.get(`/api/users/${user.pubkey}`) - - if (!userResponse.data) { - showToast('error', 'Error', 'User not found', 'Please try again.'); - return; - } - - const resourcePayload = { - id: newWorkshopId, - userId: userResponse.data.id, - price: payload.price || 0, - noteId: nAddress, - } - const response = await axios.post(`/api/resources`, resourcePayload); - - if (response.status !== 201) { - showToast('error', 'Error', 'Failed to create resource. Please try again.'); - return; - } - - const publishResponse = await publishAll(signedEvent); - - if (!publishResponse) { - showToast('error', 'Error', 'Failed to publish resource. Please try again.'); - return; - } else if (publishResponse?.failedRelays) { - publishResponse?.failedRelays.map(relay => { - showToast('warn', 'Warning', `Failed to publish to relay: ${relay}`); - }); - } - - publishResponse?.successfulRelays.map(relay => { - showToast('success', 'Success', `Published to relay: ${relay}`); - }) - } - const onUpload = (event) => { showToast('success', 'Success', 'File Uploaded'); console.log(event.files[0]); @@ -236,8 +106,8 @@ const WorkshopForm = () => {

Paid Workshop

- setChecked(e.value)} /> - {checked && ( + setIsPaidResource(e.value)} /> + {isPaidResource && (
setPrice(e.value)} placeholder="Price (sats)" /> diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index 0b9683b..f06b99e 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -332,10 +332,12 @@ export function useNostr() { ); const fetchResources = useCallback(async () => { - const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; - const hasRequiredTags = (eventData) => { - const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource"); + const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const hasRequiredTags = (tags) => { + const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); + // Check if 'resource' tag exists + const hasResource = tags.some(([tag, value]) => tag === "t" && value === "resource"); + // Return true if both tags exist return hasPlebDevs && hasResource; }; @@ -370,10 +372,12 @@ export function useNostr() { }, [subscribe]); const fetchWorkshops = useCallback(async () => { - const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; - const hasRequiredTags = (eventData) => { - const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop"); + const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const hasRequiredTags = (tags) => { + const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); + + const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop"); + return hasPlebDevs && hasWorkshop; }; @@ -384,6 +388,9 @@ export function useNostr() { filter, { onevent: (event) => { + if (event.id === "fe63bb28f3e560046f3653edff75fb1d816412e5a7a1dfdddca5494d94ff22c9") { + console.log('event:!!!!', event); + } if (hasRequiredTags(event.tags)) { workshops.push(event); } @@ -409,9 +416,14 @@ export function useNostr() { const fetchCourses = useCallback(async () => { const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; - const hasRequiredTags = (eventData) => { - const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course"); + const hasRequiredTags = (tags) => { + const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); + + const hasCourse = tags.some(([tag, value]) => tag === "t" && value === "course"); + + console.log('hasPlebDevs:', hasPlebDevs); + console.log('hasCourse:', hasCourse); + return hasPlebDevs && hasCourse; }; diff --git a/src/pages/draft/[slug].js b/src/pages/draft/[slug].js index ac404a7..d746934 100644 --- a/src/pages/draft/[slug].js +++ b/src/pages/draft/[slug].js @@ -3,7 +3,7 @@ import axios from 'axios'; import { useRouter } from 'next/router'; import { useNostr } from '@/hooks/useNostr'; import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr'; -import { verifyEvent, nip19 } from 'nostr-tools'; +import { verifyEvent, nip19, nip04 } from 'nostr-tools'; import { v4 as uuidv4 } from 'uuid'; import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; import { useImageProxy } from '@/hooks/useImageProxy'; @@ -31,7 +31,8 @@ export default function Details() { const [draft, setDraft] = useState(null); const { returnImageProxy } = useImageProxy(); - const { fetchSingleEvent, fetchKind0 } = useNostr(); + + const { publish, fetchSingleEvent } = useNostr(); const [user] = useLocalStorageWithEffect('user', {}); @@ -41,8 +42,6 @@ export default function Details() { const { showToast } = useToast(); - const { publishAll } = useNostr(); - useEffect(() => { if (router.isReady) { const { slug } = router.query; @@ -60,7 +59,7 @@ export default function Details() { const handleSubmit = async () => { if (draft) { - const { unsignedEvent, type } = buildEvent(draft); + const { unsignedEvent, type } = await buildEvent(draft); if (unsignedEvent) { await publishEvent(unsignedEvent, type); @@ -110,26 +109,55 @@ export default function Details() { return; } - await publishAll(signedEvent); + await publish(signedEvent); + + // check if the event is published + const publishedEvent = await fetchSingleEvent(signedEvent.id); + + console.log('publishedEvent:', publishedEvent); + + if (publishedEvent) { + // show success message + showToast('success', 'Success', `${type} published successfully.`); + // delete the draft + await axios.delete(`/api/drafts/${draft.id}`) + .then(res => { + if (res.status === 204) { + showToast('success', 'Success', 'Draft deleted successfully.'); + router.push(`/profile`); + } else { + showToast('error', 'Error', 'Failed to delete draft.'); + } + }) + .catch(err => { + console.error(err); + }); + } } - const buildEvent = (draft) => { + const buildEvent = async (draft) => { const NewDTag = uuidv4(); let event = {}; let type; + let encryptedContent; + console.log('draft:', draft); switch (draft?.type) { case 'resource': + 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 - content: draft.content, + content: draft?.price ? encryptedContent : draft.content, created_at: Math.floor(Date.now() / 1000), tags: [ ['d', NewDTag], ['title', draft.title], ['summary', draft.summary], ['image', draft.image], - ['t', ...draft.topics], + ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], // Include price and location tags only if price is present ...(draft?.price ? [['price', draft.price], ['location', `https://plebdevs.com/resource/${draft.id}`]] : []), @@ -138,16 +166,20 @@ export default function Details() { type = 'resource'; break; case 'workshop': + 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: 30023, - content: draft.content, + kind: draft?.price ? 30402 : 30023, + content: draft?.price ? encryptedContent : draft.content, created_at: Math.floor(Date.now() / 1000), tags: [ ['d', NewDTag], ['title', draft.title], ['summary', draft.summary], ['image', draft.image], - ['t', ...draft.topics], + ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ] }; @@ -163,7 +195,7 @@ export default function Details() { ['title', draft.title], ['summary', draft.summary], ['image', draft.image], - ['t', ...draft.topics], + ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ] };