diff --git a/package-lock.json b/package-lock.json index a174823..7749092 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,11 @@ "": { "name": "plebdevs", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@getalby/bitcoin-connect-react": "^3.5.3", "@prisma/client": "^5.17.0", + "@tanstack/react-query": "^5.51.21", "@uiw/react-markdown-preview": "^5.1.2", "@uiw/react-md-editor": "^3.11.0", "axios": "^1.7.2", @@ -1216,6 +1218,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", + "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.21.tgz", + "integrity": "sha512-Q/V81x3sAYgCsxjwOkfLXfrmoG+FmDhLeHH5okC/Bp8Aaw2c33lbEo/mMcMnkxUPVtB2FLpzHT0tq3c+OlZEbw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.51.21" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", diff --git a/package.json b/package.json index 21fc4c0..f5b200f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@getalby/bitcoin-connect-react": "^3.5.3", "@prisma/client": "^5.17.0", - "@tanstack/react-query": "^5.0.0", + "@tanstack/react-query": "^5.51.21", "@uiw/react-markdown-preview": "^5.1.2", "@uiw/react-md-editor": "^3.11.0", "axios": "^1.7.2", @@ -35,4 +35,4 @@ "postcss": "^8", "tailwindcss": "^3.4.1" } -} \ No newline at end of file +} diff --git a/src/components/content/carousels/CoursesCarousel.js b/src/components/content/carousels/CoursesCarousel.js index 4e29689..971e6cf 100644 --- a/src/components/content/carousels/CoursesCarousel.js +++ b/src/components/content/carousels/CoursesCarousel.js @@ -1,9 +1,10 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, use } from 'react'; import { Carousel } from 'primereact/carousel'; import { parseCourseEvent } from '@/utils/nostr'; import { useNostr } from '@/hooks/useNostr'; import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate'; import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton'; +import { useNostrQueries } from '@/hooks/useNostrQueries'; const responsiveOptions = [ { @@ -25,15 +26,26 @@ const responsiveOptions = [ export default function CoursesCarousel() { const [processedCourses, setProcessedCourses] = useState([]); - const { fetchCourses, fetchZapsForEvents } = useNostr(); + const { fetchZapsForEvents } = useNostr(); + const { courses, coursesError, zapsForEvents, refetchZapsForEvents } = useNostrQueries() + + useEffect(() => { + if (courses && courses.length > 0) { + refetchZapsForEvents(courses); + } + }, [courses]); + + useEffect(() => { + console.log('zapsForEvents:', zapsForEvents); + }, [zapsForEvents]); useEffect(() => { const fetch = async () => { try { - const fetchedCourses = await fetchCourses(); - if (fetchedCourses && fetchedCourses.length > 0) { + if ( courses && courses.length > 0) { + console.log('courses:', courses); // First process the courses to be ready for display - const processedCourses = fetchedCourses.map(course => parseCourseEvent(course)); + const processedCourses = courses.map(course => parseCourseEvent(course)); // Fetch zaps for all processed courses at once const allZaps = await fetchZapsForEvents(processedCourses); @@ -62,7 +74,11 @@ export default function CoursesCarousel() { } }; fetch(); - }, [fetchCourses, fetchZapsForEvents]); + }, [courses]); + + if (coursesError) { + return
Error: {coursesError.message}
+ } return ( <> diff --git a/src/components/content/carousels/ResourcesCarousel.js b/src/components/content/carousels/ResourcesCarousel.js index 0d0d0e2..60e124b 100644 --- a/src/components/content/carousels/ResourcesCarousel.js +++ b/src/components/content/carousels/ResourcesCarousel.js @@ -4,6 +4,7 @@ import { useNostr } from '@/hooks/useNostr'; import { parseEvent } from '@/utils/nostr'; import ResourceTemplate from '@/components/content/carousels/templates/ResourceTemplate'; import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton'; +import { useNostrQueries } from '@/hooks/useNostrQueries'; const responsiveOptions = [ { @@ -25,14 +26,16 @@ const responsiveOptions = [ export default function ResourcesCarousel() { const [processedResources, setProcessedResources] = useState([]); - const { fetchResources, fetchZapsForEvents } = useNostr(); + const { fetchZapsForEvents } = useNostr(); + const { resources, resourcesError, refetchResources } = useNostrQueries() useEffect(() => { const fetch = async () => { try { - const fetchedResources = await fetchResources(); - if (fetchedResources && fetchedResources.length > 0) { - const processedResources = fetchedResources.map(resource => parseEvent(resource)); + if (resources && resources.length > 0) { + const processedResources = resources.map(resource => parseEvent(resource)); + + console.log('processedResources:', processedResources); const allZaps = await fetchZapsForEvents(processedResources); @@ -51,14 +54,18 @@ export default function ResourcesCarousel() { setProcessedResources(resourcesWithZaps); } else { - console.log('No resources fetched or empty array returned'); + refetchResources(); } } catch (error) { console.error('Error fetching resources:', error); } }; fetch(); - }, [fetchResources, fetchZapsForEvents]); // Assuming fetchZapsForEvents is adjusted to handle resources + }, [resources]); + + if (resourcesError) { + return
Error: {resourcesError.message}
+ } return ( diff --git a/src/components/content/carousels/WorkshopsCarousel.js b/src/components/content/carousels/WorkshopsCarousel.js index efa0007..453bee9 100644 --- a/src/components/content/carousels/WorkshopsCarousel.js +++ b/src/components/content/carousels/WorkshopsCarousel.js @@ -1,11 +1,12 @@ import React, { useState, useEffect } from 'react'; import { Carousel } from 'primereact/carousel'; import { useRouter } from 'next/router'; -import { useNostr } from '@/hooks/useNostr'; import { useImageProxy } from '@/hooks/useImageProxy'; +import { useNostr } from '@/hooks/useNostr'; import { parseEvent } from '@/utils/nostr'; import WorkshopTemplate from '@/components/content/carousels/templates/WorkshopTemplate'; import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton'; +import { useNostrQueries } from '@/hooks/useNostrQueries'; const responsiveOptions = [ { @@ -26,15 +27,17 @@ const responsiveOptions = [ ]; export default function WorkshopsCarousel() { - const [processedWorkshops, setProcessedWorkshops] = useState([]); - const { fetchWorkshops, fetchZapsForEvents } = useNostr(); + const [processedWorkshops, setProcessedWorkshops] = useState([]) + + const { workshops, workshopsError } = useNostrQueries() + const { fetchZapsForEvents } = useNostr() useEffect(() => { const fetch = async () => { try { - const fetchedWorkshops = await fetchWorkshops(); - if (fetchedWorkshops && fetchedWorkshops.length > 0) { - const processedWorkshops = fetchedWorkshops.map(workshop => parseEvent(workshop)); + console.debug('workshops', workshops); + if (workshops && workshops.length > 0) { + const processedWorkshops = workshops.map(workshop => parseEvent(workshop)); const allZaps = await fetchZapsForEvents(processedWorkshops); @@ -60,8 +63,11 @@ export default function WorkshopsCarousel() { } }; fetch(); - }, [fetchWorkshops, fetchZapsForEvents]); // Assuming fetchZapsForEvents is adjusted to handle workshops - + }, [workshops]); + + if (workshopsError) { + return
Error: {workshopsError.message}
+ } return ( <> diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js index 30b9569..b0c4277 100644 --- a/src/hooks/useLocalStorage.js +++ b/src/hooks/useLocalStorage.js @@ -1,3 +1,4 @@ +"use client"; import { useState, useEffect } from 'react'; // This version of the hook initializes state without immediately attempting to read from localStorage diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index ed9a336..cfa52c6 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -1,6 +1,5 @@ -import { useState, useEffect, useCallback, useContext, useRef } from 'react'; +import { 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'; @@ -16,8 +15,6 @@ const defaultRelays = [ "wss://relay.primal.net/" ]; -const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY; - export function useNostr() { const pool = useContext(NostrContext); const subscriptionQueue = useRef([]); @@ -366,117 +363,6 @@ export function useNostr() { [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); @@ -561,5 +447,5 @@ export function useNostr() { [publish] ); - return { subscribe, publish, fetchSingleEvent, fetchSingleNaddrEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; + return { subscribe, publish, fetchSingleEvent, fetchSingleNaddrEvent, fetchZapsForEvent, fetchKind0, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; } \ No newline at end of file diff --git a/src/hooks/useNostrQueries.js b/src/hooks/useNostrQueries.js new file mode 100644 index 0000000..9253f85 --- /dev/null +++ b/src/hooks/useNostrQueries.js @@ -0,0 +1,164 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useState, useEffect, useCallback } from 'react' +import { useNostr } from '@/hooks/useNostr' + +const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY + +export function useNostrQueries() { + const [isClient, setIsClient] = useState(false) + + const { subscribe, fetchZapsForEvent, fetchZapsForEvents } = useNostr() + const queryClient = useQueryClient() + + useEffect(() => { + setIsClient(true) + }, []) + + const fetchWorkshops = 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) => { + let workshops = [] + const subscription = subscribe(filter, + { + onevent: (event) => { + if (hasRequiredTags(event.tags)) { + workshops.push(event) + } + }, + onerror: (error) => { + console.error('Error fetching workshops:', error) + reject(error); + }, + onclose: () => { + resolve(workshops) + }, + } + ) + + // Set a timeout to resolve the promise after collecting events + setTimeout(() => { + subscription?.close() + resolve(workshops) + }, 2000) // Adjust the timeout value as needed + }) + } + + const fetchResources = async () => { + console.log('fetching resources'); + 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); + reject(error); + }, + onclose: () => { + resolve(resources); + }, + } + ); + + // Set a timeout to resolve the promise after collecting events + setTimeout(() => { + subscription?.close(); + resolve(resources); + }, 2000); // Adjust the timeout value as needed + }); + } + + const fetchCourses = 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); + reject(error); + }, + onclose: () => { + resolve(courses); + }, + } + ); + + setTimeout(() => { + subscription?.close(); + resolve(courses); + }, 2000); + }); + } + + const { data: workshops, isLoading: workshopsLoading, error: workshopsError, refetch: refetchWorkshops } = useQuery({ + queryKey: ['workshops', isClient], + queryFn: fetchWorkshops, + staleTime: 1000 * 60 * 10, // 10 minutes + cacheTime: 1000 * 60 * 60, // 1 hour + enabled: isClient, + }) + + const { data: resources, isLoading: resourcesLoading, error: resourcesError, refetch: refetchResources } = useQuery({ + queryKey: ['resources', isClient], + queryFn: fetchResources, + staleTime: 1000 * 60 * 10, // 10 minutes + cacheTime: 1000 * 60 * 60, // 1 hour + enabled: isClient, + }) + + const { data: courses, isLoading: coursesLoading, error: coursesError, refetch: refetchCourses } = useQuery({ + queryKey: ['courses', isClient], + queryFn: fetchCourses, + staleTime: 1000 * 60 * 10, // 10 minutes + cacheTime: 1000 * 60 * 60, // 1 hour + enabled: isClient, + }) + + return { + workshops, + workshopsLoading, + workshopsError, + resources, + resourcesLoading, + resourcesError, + courses, + coursesLoading, + coursesError, + refetchCourses, + refetchResources, + refetchWorkshops, + } +} \ No newline at end of file diff --git a/src/pages/_app.js b/src/pages/_app.js index ce3e9df..ae98e4d 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -9,28 +9,35 @@ 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 { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query' + +const queryClient = new QueryClient() export default function MyApp({ Component, pageProps: { ...pageProps } }) { - return ( - - -
- - {/*
*/} - {/* */} - {/*
*/} -
- + + + +
+ + {/*
*/} + {/* */} + {/*
*/} +
+ +
+ {/*
*/}
- {/*
*/} -
- - + + + ); diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index aea81b5..c5077d2 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -124,12 +124,11 @@ export default function Details() { const payload = { id: dTag, userId: userResponse.data.id, - price: draft.price || 0, + price: Number(draft.price) || 0, noteId: nAddress, } - console.log('payload:', payload); + const response = await axios.post(`/api/resources`, payload); - console.log('response:', response); if (response.status !== 201) { showToast('error', 'Error', 'Failed to create resource. Please try again.'); @@ -145,19 +144,14 @@ export default function Details() { published = await publishCourse(signedEvent); } - console.log('published:', published); - if (published) { // 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 - console.log('draft:', draft); await axios.delete(`/api/drafts/${draft.id}`) .then(res => { if (res.status === 204) { @@ -198,7 +192,7 @@ export default function Details() { ...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/details/${draft.id}`]] : []), + ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), ] }; type = 'resource'; diff --git a/src/pages/index.js b/src/pages/index.js index 9078075..76fa4ca 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -8,21 +8,6 @@ import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; import axios from 'axios'; export default function Home() { - const [contentIds, setContentIds] = useLocalStorageWithEffect('contentIds', []); - - const fetchContentIds = useCallback(async () => { - try { - const response = await axios.get('/api/content/all'); - const ids = response.data; - setContentIds(ids); - } catch (error) { - console.error('Failed to fetch content IDs:', error); - } - }, []); - - useEffect(() => { - fetchContentIds(); - }, [fetchContentIds]); return ( <> @@ -34,9 +19,9 @@ export default function Home() {
- + {/* */} - + {/* */}
);