diff --git a/src/components/profile/UserContent.js b/src/components/profile/UserContent.js
index 24bc403..a8e5128 100644
--- a/src/components/profile/UserContent.js
+++ b/src/components/profile/UserContent.js
@@ -1,95 +1,104 @@
import React, { useState, useEffect } from "react";
-import axios from "axios";
import { useRouter } from "next/router";
import { Button } from "primereact/button";
import MenuTab from "@/components/menutab/MenuTab";
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
-import { useNostr } from "@/hooks/useNostr";
+import { useCoursesQuery } from "@/hooks/nostrQueries/content/useCoursesQuery";
+import { useResourcesQuery } from "@/hooks/nostrQueries/content/useResourcesQuery";
+import { useWorkshopsQuery } from "@/hooks/nostrQueries/content/useWorkshopsQuery";
+import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery";
+import { useContentIdsQuery } from "@/hooks/apiQueries/useContentIdsQuery";
+import { useToast } from "@/hooks/useToast";
import ContentList from "@/components/content/lists/ContentList";
import { parseEvent } from "@/utils/nostr";
-import { useToast } from "@/hooks/useToast";
+import { useNDKContext } from "@/context/NDKContext";
+
+const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
const UserContent = () => {
const [activeIndex, setActiveIndex] = useState(0);
- const [drafts, setDrafts] = useState([]);
- const [user, setUser] = useLocalStorageWithEffect('user', {});
- const [courses, setCourses] = useState([]);
- const [resources, setResources] = useState([]);
- const [workshops, setWorkshops] = useState([]);
- const { fetchCourses, fetchResources, fetchWorkshops } = useNostr();
+ const [isClient, setIsClient] = useState(false);
+ const [content, setContent] = useState([]);
+ const [publishedContent, setPublishedContent] = useState([]);
+
+ const [user] = useLocalStorageWithEffect("user", {});
const router = useRouter();
const { showToast } = useToast();
+ const ndk = useNDKContext();
+ const { courses, coursesLoading, coursesError } = useCoursesQuery();
+ const { resources, resourcesLoading, resourcesError } = useResourcesQuery();
+ const { workshops, workshopsLoading, workshopsError } = useWorkshopsQuery();
+ const { drafts, draftsLoading, draftsError } = useDraftsQuery();
+ const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
const contentItems = [
- { label: 'Published', icon: 'pi pi-verified' },
- { label: 'Drafts', icon: 'pi pi-file-edit' },
- { label: 'Resources', icon: 'pi pi-book' },
- { label: 'Workshops', icon: 'pi pi-video' },
- { label: 'Courses', icon: 'pi pi-desktop' }
+ { label: "Published", icon: "pi pi-verified" },
+ { label: "Drafts", icon: "pi pi-file-edit" },
+ { label: "Resources", icon: "pi pi-book" },
+ { label: "Workshops", icon: "pi pi-video" },
+ { label: "Courses", icon: "pi pi-desktop" },
];
useEffect(() => {
- if (user && user.id) {
- fetchAllContent();
- }
- }, [user]);
+ const fetchAllContentFromNDK = async (ids) => {
+ try {
+ await ndk.connect();
+ const filter = { "#d": ids, authors: [AUTHOR_PUBKEY] };
- const fetchAllContent = async () => {
- try {
- console.log(user.id)
- // Fetch drafts from the database
- const draftsResponse = await axios.get(`/api/drafts/all/${user.id}`);
- const drafts = draftsResponse.data;
- console.log('drafts:', drafts);
+ const uniqueEvents = new Set();
- // Fetch resources, workshops, and courses from Nostr
- const resources = await fetchResources();
- const workshops = await fetchWorkshops();
- const courses = await fetchCourses();
-
- if (drafts.length > 0) {
- setDrafts(drafts);
- }
- if (resources.length > 0) {
- setResources(resources);
- }
- if (workshops.length > 0) {
- setWorkshops(workshops);
- }
- if (courses.length > 0) {
- setCourses(courses);
- }
- } catch (err) {
- console.error(err);
- showToast('error', 'Error', 'Failed to fetch content');
- }
- };
-
- const getContentByIndex = (index) => {
- switch (index) {
- case 0:
- return []
- case 1:
- return drafts;
- case 2:
- return resources.map(resource => {
- const { id, content, title, summary, image, published_at } = parseEvent(resource);
- return { id, content, title, summary, image, published_at };
+ const events = await ndk.fetchEvents(filter);
+
+ events.forEach(event => {
+ uniqueEvents.add(event);
});
- case 3:
- return workshops.map(workshop => {
- const { id, content, title, summary, image, published_at } = parseEvent(workshop);
- return { id, content, title, summary, image, published_at };
- })
- case 4:
- return courses.map(course => {
- const { id, content, title, summary, image, published_at } = parseEvent(course);
- return { id, content, title, summary, image, published_at };
- })
- default:
+
+ console.log('uniqueEvents', uniqueEvents)
+ return Array.from(uniqueEvents);
+ } catch (error) {
+ console.error('Error fetching workshops from NDK:', error);
return [];
+ }
+ };
+
+ const fetchContent = async () => {
+ if (contentIds && isClient) {
+ const content = await fetchAllContentFromNDK(contentIds);
+ setPublishedContent(content);
+ }
}
- };
+ fetchContent();
+ }, [contentIds, isClient, ndk]);
+
+ useEffect(() => {
+ if (isClient) {
+ const getContentByIndex = (index) => {
+ switch (index) {
+ case 0:
+ return publishedContent.map(parseEvent) || [];
+ case 1:
+ return drafts || [];
+ case 2:
+ return resources?.map(parseEvent) || [];
+ case 3:
+ return workshops?.map(parseEvent) || [];
+ case 4:
+ return courses?.map(parseEvent) || [];
+ default:
+ return [];
+ }
+ };
+
+ setContent(getContentByIndex(activeIndex));
+ }
+ }, [activeIndex, isClient, drafts, resources, workshops, courses, publishedContent])
+
+ const isLoading = coursesLoading || resourcesLoading || workshopsLoading || draftsLoading || contentIdsLoading;
+ const isError = coursesError || resourcesError || workshopsError || draftsError || contentIdsError;
return (
@@ -97,13 +106,29 @@ const UserContent = () => {
Your Content
-
- router.push('/create')} label="Create" severity="success" outlined className="mt-2" />
+
+ router.push("/create")}
+ label="Create"
+ severity="success"
+ outlined
+ className="mt-2"
+ />
- {getContentByIndex(activeIndex).length > 0 && (
-
+ {isLoading ? (
+
Loading...
+ ) : isError ? (
+
Error loading content.
+ ) : content.length > 0 ? (
+
+ ) : (
+
No content available.
)}
diff --git a/src/hooks/apiQueries/useContentIdsQuery.js b/src/hooks/apiQueries/useContentIdsQuery.js
new file mode 100644
index 0000000..5767d6f
--- /dev/null
+++ b/src/hooks/apiQueries/useContentIdsQuery.js
@@ -0,0 +1,32 @@
+import { useState, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import axios from 'axios';
+
+export function useContentIdsQuery() {
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ const fetchContentIdsDB = async () => {
+ try {
+ const response = await axios.get(`/api/content/all`);
+ const contentIds = response.data;
+ return contentIds;
+ } catch (error) {
+ console.error('Error fetching contentIds from DB:', error);
+ return [];
+ }
+ };
+
+ const { data: contentIds, isLoading: contentIdsLoading, error: contentIdsError, refetch: refetchContentIds } = useQuery({
+ queryKey: ['contentIds', isClient],
+ queryFn: fetchContentIdsDB,
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ refetchInterval: 1000 * 60 * 30, // 30 minutes
+ enabled: isClient
+ });
+
+ return { contentIds, contentIdsLoading, contentIdsError, refetchContentIds };
+}
diff --git a/src/hooks/apiQueries/useDraftsQuery.js b/src/hooks/apiQueries/useDraftsQuery.js
new file mode 100644
index 0000000..f657d2f
--- /dev/null
+++ b/src/hooks/apiQueries/useDraftsQuery.js
@@ -0,0 +1,38 @@
+import { useState, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import axios from 'axios';
+import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
+
+export function useDraftsQuery() {
+ const [isClient, setIsClient] = useState(false);
+ const [user] = useLocalStorageWithEffect('user', {});
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ const fetchDraftsDB = async () => {
+ try {
+ if (!user.id) {
+ return [];
+ }
+ const response = await axios.get(`/api/drafts/all/${user.id}`);
+ const drafts = response.data;
+ console.log('drafts:', drafts);
+ return drafts;
+ } catch (error) {
+ console.error('Error fetching drafts from DB:', error);
+ return [];
+ }
+ };
+
+ const { data: drafts, isLoading: draftsLoading, error: draftsError, refetch: refetchDrafts } = useQuery({
+ queryKey: ['drafts', isClient],
+ queryFn: fetchDraftsDB,
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ refetchInterval: 1000 * 60 * 30, // 30 minutes
+ enabled: isClient && !!user.id, // Only enable if client-side and user ID is available
+ });
+
+ return { drafts, draftsLoading, draftsError, refetchDrafts };
+}
diff --git a/src/hooks/nostrQueries/content/useAllContentQuery.js b/src/hooks/nostrQueries/content/useAllContentQuery.js
new file mode 100644
index 0000000..9ea7fcb
--- /dev/null
+++ b/src/hooks/nostrQueries/content/useAllContentQuery.js
@@ -0,0 +1,41 @@
+import { useState, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { useNDKContext } from '@/context/NDKContext';
+
+export function useAllContentQuery({ids}) {
+ const [isClient, setIsClient] = useState(false);
+ const ndk = useNDKContext();
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+const fetchAllContentFromNDK = async (ids) => {
+ try {
+ console.log('Fetching all content from NDK');
+ await ndk.connect();
+
+ const filter = { ids: ids };
+ const events = await ndk.fetchEvents(filter);
+
+ if (events && events.size > 0) {
+ const eventsArray = Array.from(events);
+ return eventsArray;
+ }
+ return [];
+ } catch (error) {
+ console.error('Error fetching workshops from NDK:', error);
+ return [];
+ }
+};
+
+const { data: allContent, isLoading: allContentLoading, error: allContentError, refetch: refetchAllContent } = useQuery({
+ queryKey: ['allContent', isClient],
+ queryFn: () => fetchAllContentFromNDK(ids),
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ refetchInterval: 1000 * 60 * 30, // 30 minutes
+ enabled: isClient,
+ })
+
+ return { allContent, allContentLoading, allContentError, refetchAllContent }
+}
\ No newline at end of file
diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js
index cfa52c6..ed9a336 100644
--- a/src/hooks/useNostr.js
+++ b/src/hooks/useNostr.js
@@ -1,5 +1,6 @@
-import { useCallback, useContext, useRef } from 'react';
+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';
@@ -15,6 +16,8 @@ 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([]);
@@ -363,6 +366,117 @@ 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);
@@ -447,5 +561,5 @@ export function useNostr() {
[publish]
);
- return { subscribe, publish, fetchSingleEvent, fetchSingleNaddrEvent, fetchZapsForEvent, fetchKind0, zapEvent, fetchZapsForEvents, publishResource, publishCourse };
+ 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/details/[slug].js b/src/pages/details/[slug].js
index 4337987..b0cfd1a 100644
--- a/src/pages/details/[slug].js
+++ b/src/pages/details/[slug].js
@@ -5,7 +5,7 @@ import { useImageProxy } from '@/hooks/useImageProxy';
import { getSatAmountFromInvoice } from '@/utils/lightning';
import ZapDisplay from '@/components/zaps/ZapDisplay';
import { Tag } from 'primereact/tag';
-import { nip19 } from 'nostr-tools';
+import { nip19, nip04 } from 'nostr-tools';
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
import Image from 'next/image';
import dynamic from 'next/dynamic';
@@ -27,6 +27,8 @@ const BitcoinConnectPayButton = dynamic(
}
);
+const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY;
+
export default function Details() {
const [event, setEvent] = useState(null);
const [processedEvent, setProcessedEvent] = useState({});
@@ -34,6 +36,9 @@ export default function Details() {
const [bitcoinConnect, setBitcoinConnect] = useState(false);
const [nAddress, setNAddress] = useState(null);
const [zapAmount, setZapAmount] = useState(null);
+ const [paidResource, setPaidResource] = useState(false);
+ const [decryptedContent, setDecryptedContent] = useState(null);
+ // const [user, setUser] = useState(null);
const ndk = useNDKContext();
const [user] = useLocalStorageWithEffect('user', {});
@@ -42,6 +47,12 @@ export default function Details() {
const router = useRouter();
+ useEffect(() => {
+ if (processedEvent.price) {
+ setPaidResource(true);
+ }
+ }, [processedEvent]);
+
useEffect(() => {
if (typeof window === 'undefined') return;
@@ -52,6 +63,23 @@ export default function Details() {
}
}, []);
+ useEffect(() => {
+ const decryptContent = async () => {
+ if (user && paidResource) {
+ if (!user.purchased.includes(processedEvent.id)) {
+ // decrypt the content
+ console.log('privkey', privkey);
+ console.log('user.pubkey', user.pubkey);
+ console.log('processedEvent.content', processedEvent.content);
+ const decryptedContent = await nip04.decrypt(privkey, user.pubkey, processedEvent.content);
+ console.log('decryptedContent', decryptedContent);
+ setDecryptedContent(decryptedContent);
+ }
+ }
+ }
+ decryptContent();
+ }, [user, paidResource]);
+
useEffect(() => {
if (router.isReady) {
const { slug } = router.query;
diff --git a/src/pages/index.js b/src/pages/index.js
index e0fc9f7..1f19b08 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -6,7 +6,6 @@ import HeroBanner from '@/components/banner/HeroBanner';
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
export default function Home() {
-
return (
<>
diff --git a/src/pages/profile.js b/src/pages/profile.js
index 63f6474..661cc88 100644
--- a/src/pages/profile.js
+++ b/src/pages/profile.js
@@ -1,111 +1,98 @@
-import React, { useRef, useState, useEffect } from 'react';
+import React, { useRef, useState, useEffect } from "react";
import { Button } from "primereact/button";
-import { DataTable } from 'primereact/datatable';
-import { Menu } from 'primereact/menu';
-import { Column } from 'primereact/column';
+import { DataTable } from "primereact/datatable";
+import { Menu } from "primereact/menu";
+import { Column } from "primereact/column";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useRouter } from "next/router";
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
-import UserContent from '@/components/profile/UserContent';
+import UserContent from "@/components/profile/UserContent";
import Image from "next/image";
-import BitcoinConnectButton from '@/components/profile/BitcoinConnect';
+import BitcoinConnectButton from "@/components/profile/BitcoinConnect";
const Profile = () => {
- const [user, setUser] = useLocalStorageWithEffect('user', {});
- const { returnImageProxy } = useImageProxy();
- const menu = useRef(null);
+ const [user] = useLocalStorageWithEffect("user", {});
+ const { returnImageProxy } = useImageProxy();
+ const menu = useRef(null);
- const purchases = [
- // {
- // cost: 100,
- // name: 'Course 1',
- // category: 'Education',
- // date: '2021-09-01'
- // },
- // {
- // cost: 200,
- // name: 'Course 2',
- // category: 'Education',
- // date: '2021-09-01'
- // },
- // {
- // cost: 300,
- // name: 'Course 3',
- // category: 'Education',
- // date: '2021-09-01'
- // },
- // {
- // cost: 400,
- // name: 'Course 4',
- // category: 'Education',
- // date: '2021-09-01'
- // }
- ];
+ const purchases = [];
- const menuItems = [
- {
- label: 'Edit',
- icon: 'pi pi-pencil',
- command: () => {
- // Add your edit functionality here
- }
- },
- {
- label: 'Delete',
- icon: 'pi pi-trash',
- command: () => {
- // Add your delete functionality here
- }
- }
- ];
+ const menuItems = [
+ {
+ label: "Edit",
+ icon: "pi pi-pencil",
+ command: () => {
+ // Add your edit functionality here
+ },
+ },
+ {
+ label: "Delete",
+ icon: "pi pi-trash",
+ command: () => {
+ // Add your delete functionality here
+ },
+ },
+ ];
- const header = (
-
-
Purchases
+ const header = (
+
+ Purchases
+
+ );
+
+ return (
+ user && (
+
+
+
+
+ menu.current.toggle(e)}
+ >
+
+
+
+
+ {user.username || "Anon"}
+
+
+ {user.pubkey}
+
+
+
Connect Your Lightning Wallet
+
+
+
+
Subscription
+
You currently have no active subscription
+
+
- );
-
-
- return (
- user && (
-
-
-
-
- menu.current.toggle(e)}>
-
-
-
-
-
{user.username || "Anon"}
-
{user.pubkey}
-
-
Connect Your Lightning Wallet
-
-
-
-
Subscription
-
You currently have no active subscription
-
-
-
-
-
-
-
-
-
-
-
- )
+
+
+
+
+
+
+
+
)
-}
+ );
+};
-export default Profile
\ No newline at end of file
+export default Profile;
diff --git a/src/utils/nostr.js b/src/utils/nostr.js
index df480c5..3344289 100644
--- a/src/utils/nostr.js
+++ b/src/utils/nostr.js
@@ -67,6 +67,9 @@ export const parseEvent = (event) => {
case 'author':
eventData.author = tag[1];
break;
+ case 'price':
+ eventData.price = tag[1];
+ break;
// How do we get topics / tags?
case 'l':
// Grab index 1 and any subsequent elements in the array