From 658cfe31a9d07f3950c236d0b4a23407e3e3ec8a Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Tue, 6 Aug 2024 19:52:06 -0500 Subject: [PATCH] Completely removed useNostr hook, using NDK now --- src/components/course/CourseDetails.js | 1 - src/components/forms/CourseForm.js | 128 ++--- src/components/forms/Editor/EditorHeader.js | 46 -- src/components/forms/ResourceForm.js | 183 +------ src/context/NDKContext.js | 5 +- src/context/NostrContext.js | 43 -- src/hooks/useLogin.js | 14 +- src/hooks/useNostr.js | 565 -------------------- src/pages/_app.js | 37 +- src/pages/draft/[slug]/index.js | 312 +++++------ src/pages/profile.js | 2 + 11 files changed, 240 insertions(+), 1096 deletions(-) delete mode 100644 src/components/forms/Editor/EditorHeader.js delete mode 100644 src/context/NostrContext.js delete mode 100644 src/hooks/useNostr.js diff --git a/src/components/course/CourseDetails.js b/src/components/course/CourseDetails.js index 9cfa11e..0a94b08 100644 --- a/src/components/course/CourseDetails.js +++ b/src/components/course/CourseDetails.js @@ -53,7 +53,6 @@ export default function CourseDetails({ processedEvent }) { const author = await ndk.getUser({ pubkey }); const profile = await author.fetchProfile(); const fields = await findKind0Fields(profile); - console.log('fields:', fields); if (fields) { setAuthor(fields); } diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 4f4ce64..7c303d2 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -1,16 +1,20 @@ -import React, { useState, useEffect } from "react"; +import React, { 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 { v4 as uuidv4, v4 } from 'uuid'; import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; -import { useNostr } from "@/hooks/useNostr"; +import { useNDKContext } from "@/context/NDKContext"; import { useRouter } from "next/router"; import { useToast } from "@/hooks/useToast"; -import { nip19 } from "nostr-tools" +import { useWorkshopsQuery } from "@/hooks/nostrQueries/content/useWorkshopsQuery"; +import { useResourcesQuery } from "@/hooks/nostrQueries/content/useResourcesQuery"; +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 'primeicons/primeicons.css'; @@ -24,57 +28,25 @@ const CourseForm = () => { const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]); const [selectedLessons, setSelectedLessons] = useState([]); const [topics, setTopics] = useState(['']); - const [user, setUser] = useLocalStorageWithEffect('user', {}); - const [drafts, setDrafts] = useState([]); - const [resources, setResources] = useState([]); - const [workshops, setWorkshops] = useState([]); - const { fetchResources, fetchWorkshops, publish, fetchSingleEvent } = useNostr(); - const [pubkey, setPubkey] = useState(''); + const { resources, resourcesLoading, resourcesError } = useResourcesQuery(); + const { workshops, workshopsLoading, workshopsError } = useWorkshopsQuery(); + const { drafts, draftsLoading, draftsError } = useDraftsQuery(); + const [user, setUser] = useLocalStorageWithEffect('user', {}); + const ndk = useNDKContext(); const router = useRouter(); const { showToast } = useToast(); - const fetchAllContent = async () => { - try { - // Fetch drafts from the database - const draftsResponse = await axios.get(`/api/drafts/all/${user.id}`); - const drafts = draftsResponse.data; - - // Fetch resources and workshops from Nostr - const resources = await fetchResources(); - const workshops = await fetchWorkshops(); - - if (drafts.length > 0) { - setDrafts(drafts); - } - if (resources.length > 0) { - setResources(resources); - } - if (workshops.length > 0) { - setWorkshops(workshops); - } - } catch (err) { - console.error(err); - // Handle error - } - }; - - useEffect(() => { - if (user && user.id) { - fetchAllContent(); - } - }, [user]); - - /** - * Course Creation Flow: - * 1. Generate a new course ID - * 2. Process each lesson: - * - If unpublished: create event, publish to Nostr, save to DB, delete draft - * - If published: use existing data - * 3. Create and publish course event to Nostr - * 4. Save course to database - * 5. Show success message and redirect to course page - */ + /** + * Course Creation Flow: + * 1. Generate a new course ID + * 2. Process each lesson: + * - If unpublished: create event, publish to Nostr, save to DB, delete draft + * - If published: use existing data + * 3. Create and publish course event to Nostr + * 4. Save course to database + * 5. Show success message and redirect to course page + */ const handleSubmit = async (e) => { e.preventDefault(); @@ -90,14 +62,13 @@ const CourseForm = () => { if (!lesson.published_at) { // Publish unpublished lesson const event = createLessonEvent(lesson); - const signedEvent = await window.nostr.signEvent(event); - const published = await publish(signedEvent); + const published = await event.publish(); if (!published) { throw new Error(`Failed to publish lesson: ${lesson.title}`); } - noteId = signedEvent.id; + noteId = event.id; // Save to db and delete draft await Promise.all([ @@ -122,8 +93,9 @@ const CourseForm = () => { // Step 2: Create and publish course const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, processedLessons); - const signedCourseEvent = await window.nostr.signEvent(courseEvent); - const published = await publish(signedCourseEvent); + const published = await courseEvent.publish(); + + console.log('published', published); if (!published) { throw new Error('Failed to publish course'); @@ -136,7 +108,7 @@ const CourseForm = () => { resources: { connect: processedLessons.map(lesson => ({ id: lesson?.d })) }, - noteId: signedCourseEvent.id, + noteId: courseEvent.id, user: { connect: { id: user.id } }, @@ -148,7 +120,7 @@ const CourseForm = () => { // Step 5: Show success message and redirect showToast('success', 'Course created successfully'); - router.push(`/course/${signedCourseEvent.id}`); + router.push(`/course/${courseEvent.id}`); } catch (error) { console.error('Error creating course:', error); @@ -156,29 +128,26 @@ const CourseForm = () => { } }; - const createLessonEvent = (lesson) => ({ - kind: lesson.price ? 30402 : 30023, - content: lesson.content, - created_at: Math.floor(Date.now() / 1000), - tags: [ + 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()], - ...(lesson.price ? [ - ['price', lesson.price], - ['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`] - ] : []) - ] - }); + ]; + return event; + }; - const createCourseEvent = (courseId, title, summary, coverImage, lessons) => ({ - kind: 30004, - created_at: Math.floor(Date.now() / 1000), - content: "", - tags: [ + const createCourseEvent = (courseId, title, summary, coverImage, lessons) => { + const event = new NDKEvent(ndk); + event.kind = 30004; + event.content = ""; + event.tags = [ ['d', courseId], ['name', title], ['picture', coverImage], @@ -186,8 +155,9 @@ const CourseForm = () => { ['description', summary], ['l', "Education"], ...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]), - ], - }); + ]; + return event; + }; const handleLessonChange = (e, index) => { const selectedLessonId = e.value; @@ -235,6 +205,9 @@ const CourseForm = () => { }; const getContentOptions = (index) => { + if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) { + return []; + } const draftOptions = drafts.map(draft => ({ label: handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />, value: draft.id @@ -272,7 +245,10 @@ const CourseForm = () => { ]; }; - const lessonOptions = getContentOptions(); + // const lessonOptions = getContentOptions(); + if (resourcesLoading || workshopsLoading || draftsLoading) { + return ; + } return (
diff --git a/src/components/forms/Editor/EditorHeader.js b/src/components/forms/Editor/EditorHeader.js deleted file mode 100644 index 7dfb3fe..0000000 --- a/src/components/forms/Editor/EditorHeader.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { Button } from 'primereact/button'; - -const EditorHeader = ({ quill }) => { - const embedVideo = () => { - const videoUrl = prompt('Enter the video URL:'); - if (videoUrl) { - const videoEmbedCode = ``; - quill.editor.clipboard.dangerouslyPasteHTML(videoEmbedCode); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* This is your custom video button */} - - - - - - ); - return (
diff --git a/src/context/NDKContext.js b/src/context/NDKContext.js index 53cbfda..710a3d8 100644 --- a/src/context/NDKContext.js +++ b/src/context/NDKContext.js @@ -1,5 +1,5 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; -import NDK from "@nostr-dev-kit/ndk"; +import NDK, { NDKNip07Signer } from "@nostr-dev-kit/ndk"; const NDKContext = createContext(null); @@ -17,7 +17,8 @@ export const NDKProvider = ({ children }) => { const [ndk, setNdk] = useState(null); useEffect(() => { - const instance = new NDK({ explicitRelayUrls: relayUrls }); + const nip07signer = new NDKNip07Signer(); + const instance = new NDK({ explicitRelayUrls: relayUrls, signer: nip07signer }); setNdk(instance); }, []); diff --git a/src/context/NostrContext.js b/src/context/NostrContext.js deleted file mode 100644 index b4eb2f7..0000000 --- a/src/context/NostrContext.js +++ /dev/null @@ -1,43 +0,0 @@ -import { createContext, useState, useEffect } from 'react'; -import { SimplePool } from 'nostr-tools'; - -const defaultRelays = [ - "wss://nos.lol/", - "wss://relay.damus.io/", - "wss://relay.snort.social/", - "wss://relay.nostr.band/", - "wss://nostr.mutinywallet.com/", - "wss://relay.mutinywallet.com/", - "wss://relay.primal.net/" -]; - -export const NostrContext = createContext(); - -export const NostrProvider = ({ children }) => { - const [pool, setPool] = useState(null); - - useEffect(() => { - const newPool = new SimplePool({ verifyEvent: () => true }); - setPool(newPool); - - const connectRelays = async () => { - try { - await Promise.all(defaultRelays.map((url) => newPool.ensureRelay(url))); - } catch (error) { - console.error('Error connecting to relays:', error); - } - }; - - connectRelays(); - - return () => { - newPool.close(defaultRelays); - }; - }, []); - - return ( - - {children} - - ); -}; \ No newline at end of file diff --git a/src/hooks/useLogin.js b/src/hooks/useLogin.js index 7fc8e42..3829e26 100644 --- a/src/hooks/useLogin.js +++ b/src/hooks/useLogin.js @@ -1,15 +1,15 @@ import { useCallback, useEffect } from 'react'; import { useRouter } from 'next/router'; -import { useNostr } from './useNostr'; import axios from 'axios'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { findKind0Fields } from "@/utils/nostr"; import { useToast } from './useToast'; +import { useNDKContext } from "@/context/NDKContext"; export const useLogin = () => { const router = useRouter(); const { showToast } = useToast(); - const { fetchKind0 } = useNostr(); + const ndk = useNDKContext(); // Attempt Auto Login on render useEffect(() => { @@ -26,7 +26,8 @@ export const useLogin = () => { window.localStorage.setItem('user', JSON.stringify(response.data)); } else if (response.status === 204) { // User not found, create a new user - const kind0 = await fetchKind0(publicKey); + const author = await ndk.getUser({ pubkey: publicKey }); + const kind0 = await author.fetchProfile(); console.log('kind0:', kind0); @@ -57,7 +58,7 @@ export const useLogin = () => { }; autoLogin(); - }, []); + }, [ndk, showToast]); const nostrLogin = useCallback(async () => { if (!window || !window.nostr) { @@ -77,7 +78,8 @@ export const useLogin = () => { if (response.status === 204) { // User not found, create a new user - const kind0 = await fetchKind0(publicKey); + const author = await ndk.getUser({ pubkey: publicKey }); + const kind0 = await author.fetchProfile(); let fields = {}; if (kind0) { @@ -100,7 +102,7 @@ export const useLogin = () => { console.error('Error during login:', error); showToast('error', 'Login Error', error.message || 'Failed to log in'); } - }, [router, showToast, fetchKind0]); + }, [router, showToast, ndk]); const anonymousLogin = useCallback(() => { try { diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js deleted file mode 100644 index ed9a336..0000000 --- a/src/hooks/useNostr.js +++ /dev/null @@ -1,565 +0,0 @@ -import { useState, useEffect, useCallback, useContext, useRef } from 'react'; -import axios from 'axios'; -import { nip57, nip19 } from 'nostr-tools'; -import { NostrContext } from '@/context/NostrContext'; -import { lnurlEncode } from '@/utils/lnurl'; -import { parseEvent } from '@/utils/nostr'; -import { v4 as uuidv4 } from 'uuid'; - -const defaultRelays = [ - "wss://nos.lol/", - "wss://relay.damus.io/", - "wss://relay.snort.social/", - "wss://relay.nostr.band/", - "wss://nostr.mutinywallet.com/", - "wss://relay.mutinywallet.com/", - "wss://relay.primal.net/" -]; - -const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY; - -export function useNostr() { - const pool = useContext(NostrContext); - const subscriptionQueue = useRef([]); - const lastSubscriptionTime = useRef(0); - const throttleDelay = 2000; - - const processSubscriptionQueue = useCallback(() => { - if (subscriptionQueue.current.length === 0) return; - - const currentTime = Date.now(); - if (currentTime - lastSubscriptionTime.current < throttleDelay) { - setTimeout(processSubscriptionQueue, throttleDelay); - return; - } - - const subscription = subscriptionQueue.current.shift(); - subscription(); - - lastSubscriptionTime.current = currentTime; - setTimeout(processSubscriptionQueue, throttleDelay); - }, [throttleDelay]); - - const subscribe = useCallback( - (filters, opts) => { - if (!pool) return; - - const subscriptionFn = () => { - return pool.subscribeMany(defaultRelays, filters, opts); - }; - - subscriptionQueue.current.push(subscriptionFn); - processSubscriptionQueue(); - }, - [pool, processSubscriptionQueue] - ); - - const publish = useCallback( - async (event) => { - if (!pool) return; - - try { - await Promise.any(pool.publish(defaultRelays, event)); - console.log('Published event to at least one relay'); - return true; - } catch (error) { - console.error('Failed to publish event:', error); - return false; - } - }, - [pool] - ); - - const fetchSingleEvent = useCallback( - async (id) => { - try { - const event = await new Promise((resolve, reject) => { - subscribe( - [{ ids: [id] }], - { - onevent: (event) => { - console.log('Fetched event:', event); - resolve(event); - }, - onerror: (error) => { - console.error('Failed to fetch event:', error); - reject(error); - }, - } - ); - }); - return event; - } catch (error) { - console.error('Failed to fetch event:', error); - return null; - } - }, - [subscribe] - ); - - const fetchSingleNaddrEvent = useCallback( - async (id) => { - try { - const event = await new Promise((resolve, reject) => { - subscribe( - [{ "#d": [id] }], - { - onevent: (event) => { - resolve(event); - }, - onerror: (error) => { - console.error('Failed to fetch event:', error); - reject(error); - }, - } - ); - }); - return event; - } catch (error) { - console.error('Failed to fetch event:', error); - return null; - } - }, - [subscribe] - ); - - const querySyncQueue = useRef([]); - const lastQuerySyncTime = useRef(0); - - const processQuerySyncQueue = useCallback(() => { - if (querySyncQueue.current.length === 0) return; - - const currentTime = Date.now(); - if (currentTime - lastQuerySyncTime.current < throttleDelay) { - setTimeout(processQuerySyncQueue, throttleDelay); - return; - } - - const querySync = querySyncQueue.current.shift(); - querySync(); - - lastQuerySyncTime.current = currentTime; - setTimeout(processQuerySyncQueue, throttleDelay); - }, [throttleDelay]); - - const fetchZapsForParamaterizedEvent = useCallback( - async (kind, id, d) => { - try { - const filters = { kinds: [9735], '#a': [`${kind}:${id}:${d}`] }; - const zaps = await pool.querySync(defaultRelays, filters); - return zaps; - } catch (error) { - console.error('Failed to fetch zaps for event:', error); - return []; - } - }, - [pool] - ); - - const fetchZapsForNonParameterizedEvent = useCallback( - async (id) => { - try { - const filters = { kinds: [9735], '#e': [id] }; - const zaps = await pool.querySync(defaultRelays, filters); - return zaps; - } catch (error) { - console.error('Failed to fetch zaps for event:', error); - return []; - } - }, - [pool] - ); - - const fetchZapsForEvent = useCallback( - async (event) => { - const querySyncFn = async () => { - try { - const parameterizedZaps = await fetchZapsForParamaterizedEvent(event.kind, event.id, event.d); - const nonParameterizedZaps = await fetchZapsForNonParameterizedEvent(event.id); - return [...parameterizedZaps, ...nonParameterizedZaps]; - } catch (error) { - console.error('Failed to fetch zaps for event:', error); - return []; - } - }; - - return new Promise((resolve) => { - querySyncQueue.current.push(async () => { - const zaps = await querySyncFn(); - resolve(zaps); - }); - processQuerySyncQueue(); - }); - }, - [fetchZapsForParamaterizedEvent, fetchZapsForNonParameterizedEvent, processQuerySyncQueue] - ); - - const fetchZapsForEvents = useCallback( - async (events) => { - const querySyncFn = async () => { - try { - // Collect all #a and #e tag values from the list of events - let aTags = []; - let aTagsAlt = []; - let eTags = []; - events.forEach(event => { - aTags.push(`${event.kind}:${event.id}:${event.d}`); - aTagsAlt.push(`${event.kind}:${event.pubkey}:${event.d}`); - eTags.push(event.id); - }); - - // Create filters for batch querying - const filterA = { kinds: [9735], '#a': aTags }; - const filterE = { kinds: [9735], '#e': eTags }; - const filterAAlt = { kinds: [9735], '#a': aTagsAlt }; - - // Perform batch queries - // const [zapsA, zapsE] = await Promise.all([ - // pool.querySync(defaultRelays, filterA), - // pool.querySync(defaultRelays, filterE) - // ]); - let allZaps = [] - - await new Promise((resolve) => pool.subscribeMany(defaultRelays, [filterA, filterE, filterAAlt], { - onerror: (error) => { - console.error('Failed to fetch zaps for events:', error); - resolve([]); - }, - onevent: (event) => { - allZaps.push(event); - }, - oneose: () => { - resolve(allZaps); - } - })) - - // remove any duplicates - allZaps = allZaps.filter((zap, index, self) => index === self.findIndex((t) => ( - t.id === zap.id - ))) - - return allZaps; - } catch (error) { - console.error('Failed to fetch zaps for events:', error); - return []; - } - }; - - return new Promise((resolve) => { - querySyncQueue.current.push(async () => { - const zaps = await querySyncFn(); - resolve(zaps); - }); - processQuerySyncQueue(); - }); - }, - [pool, processQuerySyncQueue] - ); - - const fetchKind0 = useCallback( - async (publicKey) => { - return new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve(null); // Resolve with null if no event is received within the timeout - }, 10000); // 10 seconds timeout - - subscribe( - [{ authors: [publicKey], kinds: [0] }], - { - onevent: (event) => { - clearTimeout(timeout); - resolve(JSON.parse(event.content)); - }, - onerror: (error) => { - clearTimeout(timeout); - console.error('Error fetching kind 0:', error); - resolve(null); - }, - } - ); - }); - }, - [subscribe] - ); - - const zapEvent = useCallback( - async (event, amount, comment) => { - const kind0 = await fetchKind0(event.pubkey); - - if (kind0.length === 0) { - console.error('Error fetching kind0'); - return; - } - - if (kind0.lud16) { - const lud16Username = kind0.lud16.split('@')[0]; - const lud16Domain = kind0.lud16.split('@')[1]; - const lud16Url = `https://${lud16Domain}/.well-known/lnurlp/${lud16Username}`; - - try { - const response = await axios.get(lud16Url); - - if (response.data.allowsNostr) { - // const zapReq = nip57.makeZapRequest({ - // profile: event.pubkey, - // event: event.id, - // amount: amount, - // relays: defaultRelays, - // comment: comment ? comment : 'Plebdevs Zap', - // }); - - const user = window.localStorage.getItem('user'); - - const pubkey = JSON.parse(user).pubkey; - - const lnurl = lnurlEncode(lud16Url) - - console.log('lnurl:', lnurl); - - console.log('pubkey:', pubkey); - - const zapReq = { - kind: 9734, - content: "", - tags: [ - ["relays", defaultRelays.join(",")], - ["amount", amount.toString()], - // ["lnurl", lnurl], - ["e", event.id], - ["p", event.pubkey], - ["a", `${event.kind}:${event.pubkey}:${event.d}`], - ], - created_at: Math.floor(Date.now() / 1000) - } - - console.log('zapRequest:', zapReq); - - const signedEvent = await window?.nostr?.signEvent(zapReq); - console.log('signedEvent:', signedEvent); - const callbackUrl = response.data.callback; - const zapRequestAPICall = `${callbackUrl}?amount=${amount}&nostr=${encodeURI( - JSON.stringify(signedEvent) - )}`; - - const invoiceResponse = await axios.get(zapRequestAPICall); - - if (invoiceResponse?.data?.pr) { - const invoice = invoiceResponse.data.pr; - const enabled = await window?.webln?.enable(); - console.log('webln enabled:', enabled); - const payInvoiceResponse = await window?.webln?.sendPayment(invoice); - console.log('payInvoiceResponse:', payInvoiceResponse); - } else { - console.error('Error fetching invoice'); - // showToast('error', 'Error', 'Error fetching invoice'); - } - } - } catch (error) { - console.error('Error fetching lud16 data:', error); - } - } else if (profile.lud06) { - // handle lnurlpay - } else { - showToast('error', 'Error', 'User has no Lightning Address or LNURL'); - } - }, - [fetchKind0] - ); - - const fetchResources = useCallback(async () => { - const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }]; - const hasRequiredTags = (tags) => { - const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); - const hasResource = tags.some(([tag, value]) => tag === "t" && value === "resource"); - return hasPlebDevs && hasResource; - }; - - return new Promise((resolve, reject) => { - let resources = []; - const subscription = subscribe( - filter, - { - onevent: (event) => { - if (hasRequiredTags(event.tags)) { - resources.push(event); - } - }, - onerror: (error) => { - console.error('Error fetching resources:', error); - // Don't resolve here, just log the error - }, - onclose: () => { - // Don't resolve here either - }, - }, - 2000 // Adjust the timeout value as needed - ); - - // Set a timeout to resolve the promise after collecting events - setTimeout(() => { - subscription?.close(); - resolve(resources); - }, 2000); // Adjust the timeout value as needed - }); - }, [subscribe]); - - const fetchWorkshops = useCallback(async () => { - const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }]; - 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; - }; - - return new Promise((resolve, reject) => { - let workshops = []; - const subscription = subscribe( - filter, - { - onevent: (event) => { - if (hasRequiredTags(event.tags)) { - workshops.push(event); - } - }, - onerror: (error) => { - console.error('Error fetching workshops:', error); - // Don't resolve here, just log the error - }, - onclose: () => { - // Don't resolve here either - }, - }, - 2000 // Adjust the timeout value as needed - ); - - setTimeout(() => { - subscription?.close(); - resolve(workshops); - }, 2000); // Adjust the timeout value as needed - }); - }, [subscribe]); - - const fetchCourses = useCallback(async () => { - const filter = [{ kinds: [30004], authors: [AUTHOR_PUBKEY] }]; - // Do we need required tags for courses? community instead? - // const hasRequiredTags = (tags) => { - // const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); - // const hasCourse = tags.some(([tag, value]) => tag === "t" && value === "course"); - // return hasPlebDevs && hasCourse; - // }; - - return new Promise((resolve, reject) => { - let courses = []; - const subscription = subscribe( - filter, - { - onevent: (event) => { - // if (hasRequiredTags(event.tags)) { - // courses.push(event); - // } - courses.push(event); - }, - onerror: (error) => { - console.error('Error fetching courses:', error); - // Don't resolve here, just log the error - }, - onclose: () => { - // Don't resolve here either - }, - }, - 2000 // Adjust the timeout value as needed - ); - - setTimeout(() => { - subscription?.close(); - resolve(courses); - }, 2000); // Adjust the timeout value as needed - }); - }, [subscribe]); - - const publishResource = useCallback( - async (resourceEvent) => { - const published = await publish(resourceEvent); - - if (published) { - const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resourceEvent); - - const user = window.localStorage.getItem('user'); - const userId = JSON.parse(user).id; - - const payload = { - id: uuidv4(), - user: { - connect: { id: userId } // This is the correct way to connect to an existing user - }, - noteId: id - }; - - if (payload && payload.user) { - try { - const response = await axios.post('/api/resources', payload); - - if (response.status === 201) { - return true; - } - } catch (error) { - console.error('Error creating resource:', error); - return false; - } - } - } - - return false; - }, - [publish] - ); - - - const publishCourse = useCallback( - async (courseEvent) => { - const published = await publish(courseEvent); - - if (published) { - const user = window.localStorage.getItem('user'); - const pubkey = JSON.parse(user).pubkey; - - const payload = { - title: courseEvent.title, - summary: courseEvent.summary, - type: 'course', - content: courseEvent.content, - image: courseEvent.image, - user: pubkey, - topics: [...courseEvent.topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'course'] - }; - - if (payload && payload.user) { - try { - const response = await axios.post('/api/courses', payload); - - if (response.status === 201) { - try { - const deleteResponse = await axios.delete(`/api/drafts/${courseEvent.id}`); - - if (deleteResponse.status === 204) { - return true; - } - } catch (error) { - console.error('Error deleting draft:', error); - return false; - } - } - } catch (error) { - console.error('Error creating course:', error); - return false; - } - } - } - - return false; - }, - [publish] - ); - - return { subscribe, publish, fetchSingleEvent, fetchSingleNaddrEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; -} \ No newline at end of file diff --git a/src/pages/_app.js b/src/pages/_app.js index 3d1ff70..bbe0d1d 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -8,7 +8,6 @@ import 'primereact/resources/themes/lara-dark-indigo/theme.css'; import "@uiw/react-md-editor/markdown-editor.css"; import "@uiw/react-markdown-preview/markdown.css"; import Sidebar from '@/components/sidebar/Sidebar'; -import { NostrProvider } from '@/context/NostrContext'; import { NDKProvider } from '@/context/NDKContext'; import { QueryClient, @@ -22,26 +21,24 @@ export default function MyApp({ }) { return ( - - - - - -
- - {/*
*/} - {/* */} - {/*
*/} -
- -
- {/*
*/} + + + + +
+ + {/*
*/} + {/* */} + {/*
*/} +
+
- - - - - + {/*
*/} +
+ + + + ); } \ No newline at end of file diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index c5077d2..51b21d2 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -1,15 +1,16 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useRouter } from 'next/router'; -import { useNostr } from '@/hooks/useNostr'; -import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr'; -import { verifyEvent, nip19, nip04 } from 'nostr-tools'; +import { hexToNpub } from '@/utils/nostr'; +import { nip19, nip04 } from 'nostr-tools'; import { v4 as uuidv4 } from 'uuid'; import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; import { useImageProxy } from '@/hooks/useImageProxy'; import { Button } from 'primereact/button'; import { useToast } from '@/hooks/useToast'; import { Tag } from 'primereact/tag'; +import { useNDKContext } from '@/context/NDKContext'; +import { NDKEvent } from '@nostr-dev-kit/ndk'; import Image from 'next/image'; import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions'; import 'primeicons/primeicons.css'; @@ -21,20 +22,33 @@ const MDDisplay = dynamic( } ); -export default function Details() { +function validateEvent(event) { + if (typeof event.kind !== "number") return "Invalid kind"; + if (typeof event.content !== "string") return "Invalid content"; + if (typeof event.created_at !== "number") return "Invalid created_at"; + if (typeof event.pubkey !== "string") return "Invalid pubkey"; + if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return "Invalid pubkey format"; + + if (!Array.isArray(event.tags)) return "Invalid tags"; + for (let i = 0; i < event.tags.length; i++) { + const tag = event.tags[i]; + if (!Array.isArray(tag)) return "Invalid tag structure"; + for (let j = 0; j < tag.length; j++) { + if (typeof tag[j] === "object") return "Invalid tag value"; + } + } + + return true; +} + +export default function Draft() { const [draft, setDraft] = useState(null); - const { returnImageProxy } = useImageProxy(); - - const { publishCourse, publishResource, fetchSingleEvent } = useNostr(); - const [user] = useLocalStorageWithEffect('user', {}); - const { width, height } = useResponsiveImageDimensions(); - const router = useRouter(); - const { showToast } = useToast(); + const ndk = useNDKContext(); useEffect(() => { if (router.isReady) { @@ -52,32 +66,90 @@ export default function Details() { }, [router.isReady, router.query]); const handleSubmit = async () => { - if (draft) { - const { unsignedEvent, type } = await buildEvent(draft); + try { + if (draft) { + const { unsignedEvent, type } = await buildEvent(draft); - if (unsignedEvent) { - const published = await publishEvent(unsignedEvent, type); - console.log('published:', published); - // if successful, delete the draft, redirect to profile - if (published) { - 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 validationResult = validateEvent(unsignedEvent); + if (validationResult !== true) { + console.error('Invalid event:', validationResult); + showToast('error', 'Error', `Invalid event: ${validationResult}`); + return; + } + + console.log('unsignedEvent:', unsignedEvent.validate()); + console.log('unsignedEvent validation:', validationResult); + + if (unsignedEvent) { + const published = await unsignedEvent.publish(); + + const saved = await handlePostResource(unsignedEvent); + // if successful, delete the draft, redirect to profile + if (published && saved) { + 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); + }); + } + } else { + showToast('error', 'Error', 'Failed to broadcast resource. Please try again.'); } - } else { - showToast('error', 'Error', 'Failed to broadcast resource. Please try again.'); } + } catch (err) { + console.error(err); + showToast('error', 'Failed to publish resource.', err.message); } - } + }; + + const handlePostResource = async (resource) => { + console.log('resourceeeeee:', resource.tags); + const dTag = resource.tags.find(tag => tag[0] === 'd')[1]; + let price + + try { + price = resource.tags.find(tag => tag[0] === 'price')[1]; + } catch (err) { + console.error(err); + price = 0; + } + + const nAddress = nip19.naddrEncode({ + pubkey: resource.pubkey, + kind: resource.kind, + identifier: dTag, + }); + + const userResponse = await axios.get(`/api/users/${user.pubkey}`); + + if (!userResponse.data) { + showToast('error', 'Error', 'User not found', 'Please try again.'); + return; + } + + const payload = { + id: dTag, + userId: userResponse.data.id, + price: Number(price), + noteId: nAddress, + }; + + const response = await axios.post(`/api/resources`, payload); + + if (response.status !== 201) { + showToast('error', 'Error', 'Failed to create resource. Please try again.'); + return; + } + + return response.data; + }; const handleDelete = async () => { if (draft) { @@ -94,107 +166,38 @@ export default function Details() { console.error(err); }); } - } - - const publishEvent = async (event, type) => { - const dTag = event.tags.find(tag => tag[0] === 'd')[1]; - - 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: dTag, - }) - - const userResponse = await axios.get(`/api/users/${user.pubkey}`) - - if (!userResponse.data) { - showToast('error', 'Error', 'User not found', 'Please try again.'); - return; - } - - const payload = { - id: dTag, - userId: userResponse.data.id, - price: Number(draft.price) || 0, - noteId: nAddress, - } - - const response = await axios.post(`/api/resources`, payload); - - if (response.status !== 201) { - showToast('error', 'Error', 'Failed to create resource. Please try again.'); - return; - } - - let published; - console.log('type:', type); - - if (type === 'resource' || type === 'workshop') { - published = await publishResource(signedEvent); - } else if (type === 'course') { - published = await publishCourse(signedEvent); - } - - if (published) { - // check if the event is published - const publishedEvent = await fetchSingleEvent(signedEvent.id); - - 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 = async (draft) => { const NewDTag = uuidv4(); - let event = {}; + const event = new NDKEvent(ndk); let type; let encryptedContent; + console.log('Draft:', draft); + console.log('NewDTag:', NewDTag); + 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?.price ? encryptedContent : draft.content, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['d', NewDTag], - ['title', draft.title], - ['summary', draft.summary], - ['image', draft.image], - ...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.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), - ] - }; + + 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', NewDTag], + ['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}`]] : []), + ]; + type = 'resource'; break; case 'workshop': @@ -202,40 +205,40 @@ export default function Details() { // 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, - 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], - ...draft.topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()], - ] - }; + + event.kind = draft?.price ? 30402 : 30023; + event.content = draft?.price ? encryptedContent : draft.content; + event.created_at = Math.floor(Date.now() / 1000); + event.pubkey = user.pubkey; + event.tags = [ + ['d', NewDTag], + ['title', draft.title], + ['summary', draft.summary], + ['image', draft.image], + ...draft.topics.map(topic => ['t', topic]), + ['published_at', Math.floor(Date.now() / 1000).toString()], + ]; + type = 'workshop'; break; case 'course': - event = { - kind: 30023, - content: draft.content, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['d', NewDTag], - ['title', draft.title], - ['summary', draft.summary], - ['image', draft.image], - ...draft.topics.map(topic => ['t', topic]), - ['published_at', Math.floor(Date.now() / 1000).toString()], - ] - }; + event.kind = 30023; + event.content = draft.content; + event.created_at = Math.floor(Date.now() / 1000); + event.pubkey = user.pubkey; + event.tags = [ + ['d', NewDTag], + ['title', draft.title], + ['summary', draft.summary], + ['image', draft.image], + ...draft.topics.map(topic => ['t', topic]), + ['published_at', Math.floor(Date.now() / 1000).toString()], + ]; + type = 'course'; break; default: - event = null; - type = 'unknown'; + return null; } return { unsignedEvent: event, type }; @@ -244,7 +247,7 @@ export default function Details() { return (
- {/* router.push('/')} /> */} + router.push('/')} />
@@ -254,8 +257,7 @@ export default function Details() { return ( ) - }) - } + })}

{draft?.title}

{draft?.summary}

@@ -271,7 +273,7 @@ export default function Details() {

Created by{' '} - {user?.username || user?.pubkey.slice(0, 10)}{'... '} + {user?.username || user?.name || user?.pubkey.slice(0, 10)}{'... '}

)} @@ -309,4 +311,4 @@ export default function Details() {
); -} \ No newline at end of file +} diff --git a/src/pages/profile.js b/src/pages/profile.js index 661cc88..8fb8589 100644 --- a/src/pages/profile.js +++ b/src/pages/profile.js @@ -15,6 +15,8 @@ const Profile = () => { const { returnImageProxy } = useImageProxy(); const menu = useRef(null); + console.log('user:', user); + const purchases = []; const menuItems = [