Add nostr queries hook with tanstack, works with workshops, need to fix resources and courses

This commit is contained in:
austinkelsay 2024-08-03 13:42:46 -05:00
parent c622a15b89
commit ff6b6bbc2c
11 changed files with 272 additions and 178 deletions

28
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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 <div>Error: {coursesError.message}</div>
}
return (
<>

View File

@ -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 <div>Error: {resourcesError.message}</div>
}
return (

View File

@ -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 <div>Error: {workshopsError.message}</div>
}
return (
<>

View File

@ -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

View File

@ -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 };
}

View File

@ -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,
}
}

View File

@ -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 (
<PrimeReactProvider>
<NostrProvider>
<ToastProvider>
<Layout>
<div className="flex flex-col min-h-screen">
<Navbar />
{/* <div className='flex'> */}
{/* <Sidebar /> */}
{/* <div className='max-w-[100vw] pl-[15vw]'> */}
<div className='max-w-[100vw]'>
<Component {...pageProps} />
<QueryClientProvider client={queryClient}>
<ToastProvider>
<Layout>
<div className="flex flex-col min-h-screen">
<Navbar />
{/* <div className='flex'> */}
{/* <Sidebar /> */}
{/* <div className='max-w-[100vw] pl-[15vw]'> */}
<div className='max-w-[100vw]'>
<Component {...pageProps} />
</div>
{/* </div> */}
</div>
{/* </div> */}
</div>
</Layout>
</ToastProvider>
</Layout>
</ToastProvider>
</QueryClientProvider>
</NostrProvider>
</PrimeReactProvider>
);

View File

@ -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';

View File

@ -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() {
</Head>
<main>
<HeroBanner />
<CoursesCarousel />
{/* <CoursesCarousel /> */}
<WorkshopsCarousel />
<ResourcesCarousel />
{/* <ResourcesCarousel /> */}
</main>
</>
);