mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-05-18 12:15:52 +00:00
Improvements to profile pge and all user content views on profile
This commit is contained in:
parent
aa60c23611
commit
b61e927c0c
@ -1,95 +1,104 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Button } from "primereact/button";
|
import { Button } from "primereact/button";
|
||||||
import MenuTab from "@/components/menutab/MenuTab";
|
import MenuTab from "@/components/menutab/MenuTab";
|
||||||
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
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 ContentList from "@/components/content/lists/ContentList";
|
||||||
import { parseEvent } from "@/utils/nostr";
|
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 UserContent = () => {
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [drafts, setDrafts] = useState([]);
|
const [isClient, setIsClient] = useState(false);
|
||||||
const [user, setUser] = useLocalStorageWithEffect('user', {});
|
const [content, setContent] = useState([]);
|
||||||
const [courses, setCourses] = useState([]);
|
const [publishedContent, setPublishedContent] = useState([]);
|
||||||
const [resources, setResources] = useState([]);
|
|
||||||
const [workshops, setWorkshops] = useState([]);
|
const [user] = useLocalStorageWithEffect("user", {});
|
||||||
const { fetchCourses, fetchResources, fetchWorkshops } = useNostr();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showToast } = useToast();
|
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 = [
|
const contentItems = [
|
||||||
{ label: 'Published', icon: 'pi pi-verified' },
|
{ label: "Published", icon: "pi pi-verified" },
|
||||||
{ label: 'Drafts', icon: 'pi pi-file-edit' },
|
{ label: "Drafts", icon: "pi pi-file-edit" },
|
||||||
{ label: 'Resources', icon: 'pi pi-book' },
|
{ label: "Resources", icon: "pi pi-book" },
|
||||||
{ label: 'Workshops', icon: 'pi pi-video' },
|
{ label: "Workshops", icon: "pi pi-video" },
|
||||||
{ label: 'Courses', icon: 'pi pi-desktop' }
|
{ label: "Courses", icon: "pi pi-desktop" },
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && user.id) {
|
const fetchAllContentFromNDK = async (ids) => {
|
||||||
fetchAllContent();
|
try {
|
||||||
}
|
await ndk.connect();
|
||||||
}, [user]);
|
const filter = { "#d": ids, authors: [AUTHOR_PUBKEY] };
|
||||||
|
|
||||||
const fetchAllContent = async () => {
|
const uniqueEvents = new Set();
|
||||||
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);
|
|
||||||
|
|
||||||
// Fetch resources, workshops, and courses from Nostr
|
const events = await ndk.fetchEvents(filter);
|
||||||
const resources = await fetchResources();
|
|
||||||
const workshops = await fetchWorkshops();
|
|
||||||
const courses = await fetchCourses();
|
|
||||||
|
|
||||||
if (drafts.length > 0) {
|
events.forEach(event => {
|
||||||
setDrafts(drafts);
|
uniqueEvents.add(event);
|
||||||
}
|
|
||||||
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 };
|
|
||||||
});
|
});
|
||||||
case 3:
|
|
||||||
return workshops.map(workshop => {
|
console.log('uniqueEvents', uniqueEvents)
|
||||||
const { id, content, title, summary, image, published_at } = parseEvent(workshop);
|
return Array.from(uniqueEvents);
|
||||||
return { id, content, title, summary, image, published_at };
|
} catch (error) {
|
||||||
})
|
console.error('Error fetching workshops from NDK:', error);
|
||||||
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:
|
|
||||||
return [];
|
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 (
|
return (
|
||||||
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
||||||
@ -97,13 +106,29 @@ const UserContent = () => {
|
|||||||
<h2 className="text-center my-4">Your Content</h2>
|
<h2 className="text-center my-4">Your Content</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row w-full justify-between px-4">
|
<div className="flex flex-row w-full justify-between px-4">
|
||||||
<MenuTab items={contentItems} activeIndex={activeIndex} onTabChange={setActiveIndex} />
|
<MenuTab
|
||||||
<Button onClick={() => router.push('/create')} label="Create" severity="success" outlined className="mt-2" />
|
items={contentItems}
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
onTabChange={setActiveIndex}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push("/create")}
|
||||||
|
label="Create"
|
||||||
|
severity="success"
|
||||||
|
outlined
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full mx-auto my-8">
|
<div className="w-full mx-auto my-8">
|
||||||
<div className="w-full mx-auto my-8">
|
<div className="w-full mx-auto my-8">
|
||||||
{getContentByIndex(activeIndex).length > 0 && (
|
{isLoading ? (
|
||||||
<ContentList content={getContentByIndex(activeIndex)} />
|
<p>Loading...</p>
|
||||||
|
) : isError ? (
|
||||||
|
<p>Error loading content.</p>
|
||||||
|
) : content.length > 0 ? (
|
||||||
|
<ContentList content={content} />
|
||||||
|
) : (
|
||||||
|
<p>No content available.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
32
src/hooks/apiQueries/useContentIdsQuery.js
Normal file
32
src/hooks/apiQueries/useContentIdsQuery.js
Normal file
@ -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 };
|
||||||
|
}
|
38
src/hooks/apiQueries/useDraftsQuery.js
Normal file
38
src/hooks/apiQueries/useDraftsQuery.js
Normal file
@ -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 };
|
||||||
|
}
|
41
src/hooks/nostrQueries/content/useAllContentQuery.js
Normal file
41
src/hooks/nostrQueries/content/useAllContentQuery.js
Normal file
@ -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 }
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useContext, useRef } from 'react';
|
import { useState, useEffect, useCallback, useContext, useRef } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { nip57, nip19 } from 'nostr-tools';
|
||||||
import { NostrContext } from '@/context/NostrContext';
|
import { NostrContext } from '@/context/NostrContext';
|
||||||
import { lnurlEncode } from '@/utils/lnurl';
|
import { lnurlEncode } from '@/utils/lnurl';
|
||||||
import { parseEvent } from '@/utils/nostr';
|
import { parseEvent } from '@/utils/nostr';
|
||||||
@ -15,6 +16,8 @@ const defaultRelays = [
|
|||||||
"wss://relay.primal.net/"
|
"wss://relay.primal.net/"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||||
|
|
||||||
export function useNostr() {
|
export function useNostr() {
|
||||||
const pool = useContext(NostrContext);
|
const pool = useContext(NostrContext);
|
||||||
const subscriptionQueue = useRef([]);
|
const subscriptionQueue = useRef([]);
|
||||||
@ -363,6 +366,117 @@ export function useNostr() {
|
|||||||
[fetchKind0]
|
[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(
|
const publishResource = useCallback(
|
||||||
async (resourceEvent) => {
|
async (resourceEvent) => {
|
||||||
const published = await publish(resourceEvent);
|
const published = await publish(resourceEvent);
|
||||||
@ -447,5 +561,5 @@ export function useNostr() {
|
|||||||
[publish]
|
[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 };
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import { useImageProxy } from '@/hooks/useImageProxy';
|
|||||||
import { getSatAmountFromInvoice } from '@/utils/lightning';
|
import { getSatAmountFromInvoice } from '@/utils/lightning';
|
||||||
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
||||||
import { Tag } from 'primereact/tag';
|
import { Tag } from 'primereact/tag';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19, nip04 } from 'nostr-tools';
|
||||||
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
|
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import dynamic from 'next/dynamic';
|
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() {
|
export default function Details() {
|
||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
const [processedEvent, setProcessedEvent] = useState({});
|
const [processedEvent, setProcessedEvent] = useState({});
|
||||||
@ -34,6 +36,9 @@ export default function Details() {
|
|||||||
const [bitcoinConnect, setBitcoinConnect] = useState(false);
|
const [bitcoinConnect, setBitcoinConnect] = useState(false);
|
||||||
const [nAddress, setNAddress] = useState(null);
|
const [nAddress, setNAddress] = useState(null);
|
||||||
const [zapAmount, setZapAmount] = 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 ndk = useNDKContext();
|
||||||
const [user] = useLocalStorageWithEffect('user', {});
|
const [user] = useLocalStorageWithEffect('user', {});
|
||||||
@ -42,6 +47,12 @@ export default function Details() {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (processedEvent.price) {
|
||||||
|
setPaidResource(true);
|
||||||
|
}
|
||||||
|
}, [processedEvent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
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(() => {
|
useEffect(() => {
|
||||||
if (router.isReady) {
|
if (router.isReady) {
|
||||||
const { slug } = router.query;
|
const { slug } = router.query;
|
||||||
|
@ -6,7 +6,6 @@ import HeroBanner from '@/components/banner/HeroBanner';
|
|||||||
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
|
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -1,111 +1,98 @@
|
|||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { Button } from "primereact/button";
|
import { Button } from "primereact/button";
|
||||||
import { DataTable } from 'primereact/datatable';
|
import { DataTable } from "primereact/datatable";
|
||||||
import { Menu } from 'primereact/menu';
|
import { Menu } from "primereact/menu";
|
||||||
import { Column } from 'primereact/column';
|
import { Column } from "primereact/column";
|
||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
||||||
import UserContent from '@/components/profile/UserContent';
|
import UserContent from "@/components/profile/UserContent";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import BitcoinConnectButton from '@/components/profile/BitcoinConnect';
|
import BitcoinConnectButton from "@/components/profile/BitcoinConnect";
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const [user, setUser] = useLocalStorageWithEffect('user', {});
|
const [user] = useLocalStorageWithEffect("user", {});
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
const menu = useRef(null);
|
const menu = useRef(null);
|
||||||
|
|
||||||
const purchases = [
|
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 menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: "Edit",
|
||||||
icon: 'pi pi-pencil',
|
icon: "pi pi-pencil",
|
||||||
command: () => {
|
command: () => {
|
||||||
// Add your edit functionality here
|
// Add your edit functionality here
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: "Delete",
|
||||||
icon: 'pi pi-trash',
|
icon: "pi pi-trash",
|
||||||
command: () => {
|
command: () => {
|
||||||
// Add your delete functionality here
|
// Add your delete functionality here
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
|
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
<span className="text-xl text-900 font-bold text-white">Purchases</span>
|
<span className="text-xl text-900 font-bold text-white">Purchases</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
user && (
|
||||||
|
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
||||||
|
<div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
||||||
|
<div className="relative flex w-full items-center justify-center">
|
||||||
|
<Image
|
||||||
|
alt="user's avatar"
|
||||||
|
src={returnImageProxy(user.avatar, user.pubkey)}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className="rounded-full my-4"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75"
|
||||||
|
onClick={(e) => menu.current.toggle(e)}
|
||||||
|
></i>
|
||||||
|
<Menu model={menuItems} popup ref={menu} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-center text-2xl my-2">
|
||||||
|
{user.username || "Anon"}
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
|
||||||
|
{user.pubkey}
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||||
|
<h2>Connect Your Lightning Wallet</h2>
|
||||||
|
<BitcoinConnectButton />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||||
|
<h2>Subscription</h2>
|
||||||
|
<p className="text-center">You currently have no active subscription</p>
|
||||||
|
<Button
|
||||||
|
label="Subscribe"
|
||||||
|
className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<DataTable
|
||||||
|
emptyMessage="No purchases"
|
||||||
|
value={purchases}
|
||||||
return (
|
tableStyle={{ minWidth: "100%" }}
|
||||||
user && (
|
header={header}
|
||||||
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
>
|
||||||
<div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
<Column field="cost" header="Cost"></Column>
|
||||||
<div className="relative flex w-full items-center justify-center">
|
<Column field="name" header="Name"></Column>
|
||||||
<Image
|
<Column field="category" header="Category"></Column>
|
||||||
alt="user's avatar"
|
<Column field="date" header="Date"></Column>
|
||||||
src={returnImageProxy(user.avatar, user.pubkey)}
|
</DataTable>
|
||||||
width={100}
|
<UserContent />
|
||||||
height={100}
|
</div>
|
||||||
className="rounded-full my-4"
|
|
||||||
/>
|
|
||||||
<i className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75"
|
|
||||||
onClick={(e) => menu.current.toggle(e)}></i>
|
|
||||||
<Menu model={menuItems} popup ref={menu} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<h1 className="text-center text-2xl my-2">{user.username || "Anon"}</h1>
|
|
||||||
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">{user.pubkey}</h2>
|
|
||||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
|
||||||
<h2>Connect Your Lightning Wallet</h2>
|
|
||||||
<BitcoinConnectButton />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
|
||||||
<h2>Subscription</h2>
|
|
||||||
<p className="text-center">You currently have no active subscription</p>
|
|
||||||
<Button label="Subscribe" className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DataTable emptyMessage="No purchases" value={purchases} tableStyle={{ minWidth: '100%' }} header={header}>
|
|
||||||
<Column field="cost" header="Cost"></Column>
|
|
||||||
<Column field="name" header="Name"></Column>
|
|
||||||
<Column field="category" header="Category"></Column>
|
|
||||||
<Column field="date" header="Date"></Column>
|
|
||||||
</DataTable>
|
|
||||||
<UserContent />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Profile
|
export default Profile;
|
||||||
|
@ -67,6 +67,9 @@ export const parseEvent = (event) => {
|
|||||||
case 'author':
|
case 'author':
|
||||||
eventData.author = tag[1];
|
eventData.author = tag[1];
|
||||||
break;
|
break;
|
||||||
|
case 'price':
|
||||||
|
eventData.price = tag[1];
|
||||||
|
break;
|
||||||
// How do we get topics / tags?
|
// How do we get topics / tags?
|
||||||
case 'l':
|
case 'l':
|
||||||
// Grab index 1 and any subsequent elements in the array
|
// Grab index 1 and any subsequent elements in the array
|
||||||
|
Loading…
x
Reference in New Issue
Block a user