Continuing to tie together the drafts forms and course form and working on paid content

This commit is contained in:
austinkelsay 2024-04-29 14:48:15 -05:00
parent 6572f8ec99
commit 7ce97d158c
4 changed files with 112 additions and 199 deletions

View File

@ -85,7 +85,7 @@ const CourseForm = () => {
['title', lesson.title], ['title', lesson.title],
['summary', lesson.summary], ['summary', lesson.summary],
['image', lesson.image], ['image', lesson.image],
['t', ...lesson.topics], ...lesson.topics.map(topic => ['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()], ['published_at', Math.floor(Date.now() / 1000).toString()],
['price', lesson.price], ['price', lesson.price],
['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`], ['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`],
@ -101,7 +101,7 @@ const CourseForm = () => {
['title', lesson.title], ['title', lesson.title],
['summary', lesson.summary], ['summary', lesson.summary],
['image', lesson.image], ['image', lesson.image],
['t', ...lesson.topics], ...lesson.topics.map(topic => ['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()] ['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 // // Parse the fields from the lessons to get all of the necessary information
const parsedLessons = fetchedLessons.map((lesson) => { 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 { return {
id, id,
kind,
pubkey, pubkey,
content, content,
title, title,
@ -153,26 +154,24 @@ const CourseForm = () => {
const courseEvent = { const courseEvent = {
kind: 30005, kind: 30005,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
content: JSON.stringify({ content: "",
title,
summary,
price,
topics,
}),
tags: [ tags: [
['d', uuidv4()], ['d', uuidv4()],
['name', title], ['name', title],
['picture', coverImage], ['picture', coverImage],
['about', summary], ['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); 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 // Reset the form fields after publishing the course
setTitle(''); setTitle('');

View File

@ -1,13 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import router from 'next/router';
import { InputText } from 'primereact/inputtext'; import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber'; import { InputNumber } from 'primereact/inputnumber';
import { InputSwitch } from 'primereact/inputswitch'; 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 { Button } from 'primereact/button';
import { v4 as uuidv4 } from 'uuid';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
@ -15,19 +12,19 @@ import 'primeicons/primeicons.css';
const WorkshopForm = () => { const WorkshopForm = () => {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [summary, setSummary] = useState(''); const [summary, setSummary] = useState('');
const [checked, setChecked] = useState(false);
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
const [isPaidResource, setIsPaidResource] = useState(false);
const [videoUrl, setVideoUrl] = useState(''); const [videoUrl, setVideoUrl] = useState('');
const [coverImage, setCoverImage] = useState(''); const [coverImage, setCoverImage] = useState('');
const [topics, setTopics] = useState(['']); const [topics, setTopics] = useState(['']);
const router = useRouter();
const [user] = useLocalStorageWithEffect('user', {}); const [user] = useLocalStorageWithEffect('user', {});
const { showToast } = useToast(); const { showToast } = useToast();
const { publishAll } = useNostr(); const handleSubmit = async (e) => {
const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
let embedCode = ''; let embedCode = '';
@ -43,168 +40,41 @@ const WorkshopForm = () => {
} }
// Add more conditions here for other video services // 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 = { const payload = {
title, title,
summary, summary,
isPaidResource: checked, type: 'workshop',
price: checked ? price : null, price: isPaidResource ? price : null,
embedCode, content: embedCode,
image: coverImage,
user: userResponse.data.id,
topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'workshop'] topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'workshop']
}; };
console.log(payload);
if (checked) { if (payload && payload.user) {
broadcastPaidWorkshop(payload); axios.post('/api/drafts', payload)
} else { .then(response => {
broadcastFreeWorkshop(payload); 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) => { const onUpload = (event) => {
showToast('success', 'Success', 'File Uploaded'); showToast('success', 'Success', 'File Uploaded');
console.log(event.files[0]); console.log(event.files[0]);
@ -236,8 +106,8 @@ const WorkshopForm = () => {
<div className="p-inputgroup flex-1 mt-8 flex-col"> <div className="p-inputgroup flex-1 mt-8 flex-col">
<p className="py-2">Paid Workshop</p> <p className="py-2">Paid Workshop</p>
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} /> <InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
{checked && ( {isPaidResource && (
<div className="p-inputgroup flex-1 py-4"> <div className="p-inputgroup flex-1 py-4">
<i className="pi pi-bolt p-inputgroup-addon text-2xl text-yellow-500"></i> <i className="pi pi-bolt p-inputgroup-addon text-2xl text-yellow-500"></i>
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" /> <InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />

View File

@ -332,10 +332,12 @@ export function useNostr() {
); );
const fetchResources = useCallback(async () => { const fetchResources = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const hasRequiredTags = (eventData) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource"); // Check if 'resource' tag exists
const hasResource = tags.some(([tag, value]) => tag === "t" && value === "resource");
// Return true if both tags exist
return hasPlebDevs && hasResource; return hasPlebDevs && hasResource;
}; };
@ -370,10 +372,12 @@ export function useNostr() {
}, [subscribe]); }, [subscribe]);
const fetchWorkshops = useCallback(async () => { const fetchWorkshops = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const hasRequiredTags = (eventData) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop");
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop");
return hasPlebDevs && hasWorkshop; return hasPlebDevs && hasWorkshop;
}; };
@ -384,6 +388,9 @@ export function useNostr() {
filter, filter,
{ {
onevent: (event) => { onevent: (event) => {
if (event.id === "fe63bb28f3e560046f3653edff75fb1d816412e5a7a1dfdddca5494d94ff22c9") {
console.log('event:!!!!', event);
}
if (hasRequiredTags(event.tags)) { if (hasRequiredTags(event.tags)) {
workshops.push(event); workshops.push(event);
} }
@ -409,9 +416,14 @@ export function useNostr() {
const fetchCourses = useCallback(async () => { const fetchCourses = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const hasRequiredTags = (eventData) => { const hasRequiredTags = (tags) => {
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course");
const hasCourse = tags.some(([tag, value]) => tag === "t" && value === "course");
console.log('hasPlebDevs:', hasPlebDevs);
console.log('hasCourse:', hasCourse);
return hasPlebDevs && hasCourse; return hasPlebDevs && hasCourse;
}; };

View File

@ -3,7 +3,7 @@ import axios from 'axios';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useNostr } from '@/hooks/useNostr'; import { useNostr } from '@/hooks/useNostr';
import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr'; 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 { v4 as uuidv4 } from 'uuid';
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
import { useImageProxy } from '@/hooks/useImageProxy'; import { useImageProxy } from '@/hooks/useImageProxy';
@ -31,7 +31,8 @@ export default function Details() {
const [draft, setDraft] = useState(null); const [draft, setDraft] = useState(null);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { fetchSingleEvent, fetchKind0 } = useNostr();
const { publish, fetchSingleEvent } = useNostr();
const [user] = useLocalStorageWithEffect('user', {}); const [user] = useLocalStorageWithEffect('user', {});
@ -41,8 +42,6 @@ export default function Details() {
const { showToast } = useToast(); const { showToast } = useToast();
const { publishAll } = useNostr();
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
const { slug } = router.query; const { slug } = router.query;
@ -60,7 +59,7 @@ export default function Details() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (draft) { if (draft) {
const { unsignedEvent, type } = buildEvent(draft); const { unsignedEvent, type } = await buildEvent(draft);
if (unsignedEvent) { if (unsignedEvent) {
await publishEvent(unsignedEvent, type); await publishEvent(unsignedEvent, type);
@ -110,26 +109,55 @@ export default function Details() {
return; 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(); const NewDTag = uuidv4();
let event = {}; let event = {};
let type; let type;
let encryptedContent;
console.log('draft:', draft);
switch (draft?.type) { switch (draft?.type) {
case 'resource': 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 = { event = {
kind: draft?.price ? 30402 : 30023, // Determine kind based on if price is present 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), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
['d', NewDTag], ['d', NewDTag],
['title', draft.title], ['title', draft.title],
['summary', draft.summary], ['summary', draft.summary],
['image', draft.image], ['image', draft.image],
['t', ...draft.topics], ...draft.topics.map(topic => ['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()], ['published_at', Math.floor(Date.now() / 1000).toString()],
// Include price and location tags only if price is present // Include price and location tags only if price is present
...(draft?.price ? [['price', draft.price], ['location', `https://plebdevs.com/resource/${draft.id}`]] : []), ...(draft?.price ? [['price', draft.price], ['location', `https://plebdevs.com/resource/${draft.id}`]] : []),
@ -138,16 +166,20 @@ export default function Details() {
type = 'resource'; type = 'resource';
break; break;
case 'workshop': 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 = { event = {
kind: 30023, kind: draft?.price ? 30402 : 30023,
content: draft.content, content: draft?.price ? encryptedContent : draft.content,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
['d', NewDTag], ['d', NewDTag],
['title', draft.title], ['title', draft.title],
['summary', draft.summary], ['summary', draft.summary],
['image', draft.image], ['image', draft.image],
['t', ...draft.topics], ...draft.topics.map(topic => ['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()], ['published_at', Math.floor(Date.now() / 1000).toString()],
] ]
}; };
@ -163,7 +195,7 @@ export default function Details() {
['title', draft.title], ['title', draft.title],
['summary', draft.summary], ['summary', draft.summary],
['image', draft.image], ['image', draft.image],
['t', ...draft.topics], ...draft.topics.map(topic => ['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()], ['published_at', Math.floor(Date.now() / 1000).toString()],
] ]
}; };