mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Completely removed useNostr hook, using NDK now
This commit is contained in:
parent
7b69ccfb66
commit
658cfe31a9
@ -53,7 +53,6 @@ export default function CourseDetails({ processedEvent }) {
|
||||
const author = await ndk.getUser({ pubkey });
|
||||
const profile = await author.fetchProfile();
|
||||
const fields = await findKind0Fields(profile);
|
||||
console.log('fields:', fields);
|
||||
if (fields) {
|
||||
setAuthor(fields);
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { InputText } from "primereact/inputtext";
|
||||
import { InputNumber } from "primereact/inputnumber";
|
||||
import { InputSwitch } from "primereact/inputswitch";
|
||||
import { Button } from "primereact/button";
|
||||
import { Dropdown } from "primereact/dropdown";
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
import { v4 as uuidv4, v4 } from 'uuid';
|
||||
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
||||
import { useNostr } from "@/hooks/useNostr";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useRouter } from "next/router";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { nip19 } from "nostr-tools"
|
||||
import { useWorkshopsQuery } from "@/hooks/nostrQueries/content/useWorkshopsQuery";
|
||||
import { useResourcesQuery } from "@/hooks/nostrQueries/content/useResourcesQuery";
|
||||
import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { parseEvent } from "@/utils/nostr";
|
||||
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
|
||||
import 'primeicons/primeicons.css';
|
||||
@ -24,57 +28,25 @@ const CourseForm = () => {
|
||||
const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]);
|
||||
const [selectedLessons, setSelectedLessons] = useState([]);
|
||||
const [topics, setTopics] = useState(['']);
|
||||
const [user, setUser] = useLocalStorageWithEffect('user', {});
|
||||
const [drafts, setDrafts] = useState([]);
|
||||
const [resources, setResources] = useState([]);
|
||||
const [workshops, setWorkshops] = useState([]);
|
||||
const { fetchResources, fetchWorkshops, publish, fetchSingleEvent } = useNostr();
|
||||
const [pubkey, setPubkey] = useState('');
|
||||
|
||||
const { resources, resourcesLoading, resourcesError } = useResourcesQuery();
|
||||
const { workshops, workshopsLoading, workshopsError } = useWorkshopsQuery();
|
||||
const { drafts, draftsLoading, draftsError } = useDraftsQuery();
|
||||
const [user, setUser] = useLocalStorageWithEffect('user', {});
|
||||
const ndk = useNDKContext();
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const fetchAllContent = async () => {
|
||||
try {
|
||||
// Fetch drafts from the database
|
||||
const draftsResponse = await axios.get(`/api/drafts/all/${user.id}`);
|
||||
const drafts = draftsResponse.data;
|
||||
|
||||
// Fetch resources and workshops from Nostr
|
||||
const resources = await fetchResources();
|
||||
const workshops = await fetchWorkshops();
|
||||
|
||||
if (drafts.length > 0) {
|
||||
setDrafts(drafts);
|
||||
}
|
||||
if (resources.length > 0) {
|
||||
setResources(resources);
|
||||
}
|
||||
if (workshops.length > 0) {
|
||||
setWorkshops(workshops);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// Handle error
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.id) {
|
||||
fetchAllContent();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
/**
|
||||
* Course Creation Flow:
|
||||
* 1. Generate a new course ID
|
||||
* 2. Process each lesson:
|
||||
* - If unpublished: create event, publish to Nostr, save to DB, delete draft
|
||||
* - If published: use existing data
|
||||
* 3. Create and publish course event to Nostr
|
||||
* 4. Save course to database
|
||||
* 5. Show success message and redirect to course page
|
||||
*/
|
||||
/**
|
||||
* Course Creation Flow:
|
||||
* 1. Generate a new course ID
|
||||
* 2. Process each lesson:
|
||||
* - If unpublished: create event, publish to Nostr, save to DB, delete draft
|
||||
* - If published: use existing data
|
||||
* 3. Create and publish course event to Nostr
|
||||
* 4. Save course to database
|
||||
* 5. Show success message and redirect to course page
|
||||
*/
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@ -90,14 +62,13 @@ const CourseForm = () => {
|
||||
if (!lesson.published_at) {
|
||||
// Publish unpublished lesson
|
||||
const event = createLessonEvent(lesson);
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
const published = await publish(signedEvent);
|
||||
const published = await event.publish();
|
||||
|
||||
if (!published) {
|
||||
throw new Error(`Failed to publish lesson: ${lesson.title}`);
|
||||
}
|
||||
|
||||
noteId = signedEvent.id;
|
||||
noteId = event.id;
|
||||
|
||||
// Save to db and delete draft
|
||||
await Promise.all([
|
||||
@ -122,8 +93,9 @@ const CourseForm = () => {
|
||||
|
||||
// Step 2: Create and publish course
|
||||
const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, processedLessons);
|
||||
const signedCourseEvent = await window.nostr.signEvent(courseEvent);
|
||||
const published = await publish(signedCourseEvent);
|
||||
const published = await courseEvent.publish();
|
||||
|
||||
console.log('published', published);
|
||||
|
||||
if (!published) {
|
||||
throw new Error('Failed to publish course');
|
||||
@ -136,7 +108,7 @@ const CourseForm = () => {
|
||||
resources: {
|
||||
connect: processedLessons.map(lesson => ({ id: lesson?.d }))
|
||||
},
|
||||
noteId: signedCourseEvent.id,
|
||||
noteId: courseEvent.id,
|
||||
user: {
|
||||
connect: { id: user.id }
|
||||
},
|
||||
@ -148,7 +120,7 @@ const CourseForm = () => {
|
||||
|
||||
// Step 5: Show success message and redirect
|
||||
showToast('success', 'Course created successfully');
|
||||
router.push(`/course/${signedCourseEvent.id}`);
|
||||
router.push(`/course/${courseEvent.id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating course:', error);
|
||||
@ -156,29 +128,26 @@ const CourseForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const createLessonEvent = (lesson) => ({
|
||||
kind: lesson.price ? 30402 : 30023,
|
||||
content: lesson.content,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
const createLessonEvent = (lesson) => {
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = lesson.price ? 30402 : 30023;
|
||||
event.content = lesson.content;
|
||||
event.tags = [
|
||||
['d', lesson.id],
|
||||
['title', lesson.title],
|
||||
['summary', lesson.summary],
|
||||
['image', lesson.image],
|
||||
...lesson.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
...(lesson.price ? [
|
||||
['price', lesson.price],
|
||||
['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`]
|
||||
] : [])
|
||||
]
|
||||
});
|
||||
];
|
||||
return event;
|
||||
};
|
||||
|
||||
const createCourseEvent = (courseId, title, summary, coverImage, lessons) => ({
|
||||
kind: 30004,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: "",
|
||||
tags: [
|
||||
const createCourseEvent = (courseId, title, summary, coverImage, lessons) => {
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 30004;
|
||||
event.content = "";
|
||||
event.tags = [
|
||||
['d', courseId],
|
||||
['name', title],
|
||||
['picture', coverImage],
|
||||
@ -186,8 +155,9 @@ const CourseForm = () => {
|
||||
['description', summary],
|
||||
['l', "Education"],
|
||||
...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]),
|
||||
],
|
||||
});
|
||||
];
|
||||
return event;
|
||||
};
|
||||
|
||||
const handleLessonChange = (e, index) => {
|
||||
const selectedLessonId = e.value;
|
||||
@ -235,6 +205,9 @@ const CourseForm = () => {
|
||||
};
|
||||
|
||||
const getContentOptions = (index) => {
|
||||
if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) {
|
||||
return [];
|
||||
}
|
||||
const draftOptions = drafts.map(draft => ({
|
||||
label: <ContentDropdownItem content={draft} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />,
|
||||
value: draft.id
|
||||
@ -272,7 +245,10 @@ const CourseForm = () => {
|
||||
];
|
||||
};
|
||||
|
||||
const lessonOptions = getContentOptions();
|
||||
// const lessonOptions = getContentOptions();
|
||||
if (resourcesLoading || workshopsLoading || draftsLoading) {
|
||||
return <ProgressSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
|
||||
const EditorHeader = ({ quill }) => {
|
||||
const embedVideo = () => {
|
||||
const videoUrl = prompt('Enter the video URL:');
|
||||
if (videoUrl) {
|
||||
const videoEmbedCode = `<iframe width="560" height="315" src="${videoUrl}" frameborder="0" allowfullscreen></iframe>`;
|
||||
quill.editor.clipboard.dangerouslyPasteHTML(videoEmbedCode);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className="ql-formats">
|
||||
<select className="ql-font"></select>
|
||||
<select className="ql-size"></select>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-bold"></button>
|
||||
<button className="ql-italic"></button>
|
||||
<button className="ql-underline"></button>
|
||||
<select className="ql-color"></select>
|
||||
<select className="ql-background"></select>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-list" value="ordered"></button>
|
||||
<button className="ql-list" value="bullet"></button>
|
||||
<select className="ql-align"></select>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-link"></button>
|
||||
<button className="ql-image"></button>
|
||||
<button className="ql-video"></button>
|
||||
</span>
|
||||
<Button
|
||||
icon="pi pi-video"
|
||||
className="p-button-outlined p-button-secondary"
|
||||
onClick={embedVideo}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorHeader;
|
@ -4,10 +4,8 @@ import { InputText } from "primereact/inputtext";
|
||||
import { InputNumber } from "primereact/inputnumber";
|
||||
import { InputSwitch } from "primereact/inputswitch";
|
||||
import { Button } from "primereact/button";
|
||||
import { useRouter } from "next/router";
|
||||
import { useNostr } from "@/hooks/useNostr";
|
||||
import { useRouter } from "next/router";;
|
||||
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
||||
import EditorHeader from "./Editor/EditorHeader";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import dynamic from 'next/dynamic';
|
||||
const MDEditor = dynamic(
|
||||
@ -29,7 +27,6 @@ const ResourceForm = ({ draft = null }) => {
|
||||
|
||||
const [user] = useLocalStorageWithEffect('user', {});
|
||||
const { showToast } = useToast();
|
||||
const { publishAll } = useNostr();
|
||||
const router = useRouter();
|
||||
|
||||
const handleContentChange = useCallback((value) => {
|
||||
@ -94,151 +91,6 @@ const ResourceForm = ({ draft = null }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// const saveFreeResource = async (payload) => {
|
||||
// const newresourceId = uuidv4();
|
||||
// const event = {
|
||||
// kind: 30023,
|
||||
// content: payload.content,
|
||||
// created_at: Math.floor(Date.now() / 1000),
|
||||
// tags: [
|
||||
// ['d', newresourceId],
|
||||
// ['title', payload.title],
|
||||
// ['summary', payload.summary],
|
||||
// ['image', ''],
|
||||
// ['t', ...topics],
|
||||
// ['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
// ]
|
||||
// };
|
||||
|
||||
// const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
// const eventVerification = await verifyEvent(signedEvent);
|
||||
|
||||
// if (!eventVerification) {
|
||||
// showToast('error', 'Error', 'Event verification failed. Please try again.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const nAddress = nip19.naddrEncode({
|
||||
// pubkey: signedEvent.pubkey,
|
||||
// kind: signedEvent.kind,
|
||||
// identifier: newresourceId,
|
||||
// })
|
||||
|
||||
// console.log('nAddress:', nAddress);
|
||||
|
||||
// const userResponse = await axios.get(`/api/users/${user.pubkey}`)
|
||||
|
||||
// if (!userResponse.data) {
|
||||
// showToast('error', 'Error', 'User not found', 'Please try again.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const resourcePayload = {
|
||||
// id: newresourceId,
|
||||
// userId: userResponse.data.id,
|
||||
// price: 0,
|
||||
// noteId: nAddress,
|
||||
// }
|
||||
// const response = await axios.post(`/api/resources`, resourcePayload);
|
||||
|
||||
// console.log('response:', response);
|
||||
|
||||
// if (response.status !== 201) {
|
||||
// showToast('error', 'Error', 'Failed to create resource. Please try again.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const publishResponse = await publishAll(signedEvent);
|
||||
|
||||
// if (!publishResponse) {
|
||||
// showToast('error', 'Error', 'Failed to publish resource. Please try again.');
|
||||
// return;
|
||||
// } else if (publishResponse?.failedRelays) {
|
||||
// publishResponse?.failedRelays.map(relay => {
|
||||
// showToast('warn', 'Warning', `Failed to publish to relay: ${relay}`);
|
||||
// });
|
||||
// }
|
||||
|
||||
// publishResponse?.successfulRelays.map(relay => {
|
||||
// showToast('success', 'Success', `Published to relay: ${relay}`);
|
||||
// })
|
||||
// }
|
||||
|
||||
// // For images, whether included in the markdown content or not, clients SHOULD use image tags as described in NIP-58. This allows clients to display images in carousel format more easily.
|
||||
// const savePaidResource = async (payload) => {
|
||||
// // encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
||||
// const encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY ,process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, payload.content);
|
||||
// const newresourceId = uuidv4();
|
||||
// const event = {
|
||||
// kind: 30402,
|
||||
// content: encryptedContent,
|
||||
// created_at: Math.floor(Date.now() / 1000),
|
||||
// tags: [
|
||||
// ['title', payload.title],
|
||||
// ['summary', payload.summary],
|
||||
// ['t', ...topics],
|
||||
// ['image', ''],
|
||||
// ['d', newresourceId],
|
||||
// ['location', `https://plebdevs.com/resource/${newresourceId}`],
|
||||
// ['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
// ['price', payload.price]
|
||||
// ]
|
||||
// };
|
||||
|
||||
// const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
// const eventVerification = await verifyEvent(signedEvent);
|
||||
|
||||
// if (!eventVerification) {
|
||||
// showToast('error', 'Error', 'Event verification failed. Please try again.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const nAddress = nip19.naddrEncode({
|
||||
// pubkey: signedEvent.pubkey,
|
||||
// kind: signedEvent.kind,
|
||||
// identifier: newresourceId,
|
||||
// })
|
||||
|
||||
// console.log('nAddress:', nAddress);
|
||||
|
||||
// const userResponse = await axios.get(`/api/users/${user.pubkey}`)
|
||||
|
||||
// if (!userResponse.data) {
|
||||
// showToast('error', 'Error', 'User not found', 'Please try again.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const resourcePayload = {
|
||||
// id: newresourceId,
|
||||
// userId: userResponse.data.id,
|
||||
// price: payload.price || 0,
|
||||
// noteId: nAddress,
|
||||
// }
|
||||
// const response = await axios.post(`/api/resources`, resourcePayload);
|
||||
|
||||
// if (response.status !== 201) {
|
||||
// showToast('error', 'Error', 'Failed to create resource. Please try again.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const publishResponse = await publishAll(signedEvent);
|
||||
|
||||
// if (!publishResponse) {
|
||||
// showToast('error', 'Error', 'Failed to publish resource. Please try again.');
|
||||
// return;
|
||||
// } else if (publishResponse?.failedRelays) {
|
||||
// publishResponse?.failedRelays.map(relay => {
|
||||
// showToast('warn', 'Warning', `Failed to publish to relay: ${relay}`);
|
||||
// });
|
||||
// }
|
||||
|
||||
// publishResponse?.successfulRelays.map(relay => {
|
||||
// showToast('success', 'Success', `Published to relay: ${relay}`);
|
||||
// })
|
||||
// }
|
||||
|
||||
const handleTopicChange = (index, value) => {
|
||||
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
||||
setTopics(updatedTopics);
|
||||
@ -255,39 +107,6 @@ const ResourceForm = ({ draft = null }) => {
|
||||
setTopics(updatedTopics);
|
||||
};
|
||||
|
||||
// Define custom toolbar for the editor
|
||||
const customToolbar = (
|
||||
<div id="toolbar">
|
||||
{/* Include existing toolbar items */}
|
||||
<span className="ql-formats">
|
||||
<select className="ql-header" defaultValue="">
|
||||
<option value="1">Heading</option>
|
||||
<option value="2">Subheading</option>
|
||||
<option value="">Normal</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-bold"></button>
|
||||
<button className="ql-italic"></button>
|
||||
<button className="ql-underline"></button>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-list" value="ordered"></button>
|
||||
<button className="ql-list" value="bullet"></button>
|
||||
<button className="ql-indent" value="-1"></button>
|
||||
<button className="ql-indent" value="+1"></button>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-link"></button>
|
||||
<button className="ql-image"></button>
|
||||
<button className="ql-video"></button> {/* This is your custom video button */}
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-clean"></button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="p-inputgroup flex-1">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import NDK from "@nostr-dev-kit/ndk";
|
||||
import NDK, { NDKNip07Signer } from "@nostr-dev-kit/ndk";
|
||||
|
||||
const NDKContext = createContext(null);
|
||||
|
||||
@ -17,7 +17,8 @@ export const NDKProvider = ({ children }) => {
|
||||
const [ndk, setNdk] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const instance = new NDK({ explicitRelayUrls: relayUrls });
|
||||
const nip07signer = new NDKNip07Signer();
|
||||
const instance = new NDK({ explicitRelayUrls: relayUrls, signer: nip07signer });
|
||||
setNdk(instance);
|
||||
}, []);
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { createContext, useState, useEffect } from 'react';
|
||||
import { SimplePool } from 'nostr-tools';
|
||||
|
||||
const defaultRelays = [
|
||||
"wss://nos.lol/",
|
||||
"wss://relay.damus.io/",
|
||||
"wss://relay.snort.social/",
|
||||
"wss://relay.nostr.band/",
|
||||
"wss://nostr.mutinywallet.com/",
|
||||
"wss://relay.mutinywallet.com/",
|
||||
"wss://relay.primal.net/"
|
||||
];
|
||||
|
||||
export const NostrContext = createContext();
|
||||
|
||||
export const NostrProvider = ({ children }) => {
|
||||
const [pool, setPool] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const newPool = new SimplePool({ verifyEvent: () => true });
|
||||
setPool(newPool);
|
||||
|
||||
const connectRelays = async () => {
|
||||
try {
|
||||
await Promise.all(defaultRelays.map((url) => newPool.ensureRelay(url)));
|
||||
} catch (error) {
|
||||
console.error('Error connecting to relays:', error);
|
||||
}
|
||||
};
|
||||
|
||||
connectRelays();
|
||||
|
||||
return () => {
|
||||
newPool.close(defaultRelays);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NostrContext.Provider value={pool}>
|
||||
{children}
|
||||
</NostrContext.Provider>
|
||||
);
|
||||
};
|
@ -1,15 +1,15 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useNostr } from './useNostr';
|
||||
import axios from 'axios';
|
||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||
import { findKind0Fields } from "@/utils/nostr";
|
||||
import { useToast } from './useToast';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
|
||||
export const useLogin = () => {
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
const { fetchKind0 } = useNostr();
|
||||
const ndk = useNDKContext();
|
||||
|
||||
// Attempt Auto Login on render
|
||||
useEffect(() => {
|
||||
@ -26,7 +26,8 @@ export const useLogin = () => {
|
||||
window.localStorage.setItem('user', JSON.stringify(response.data));
|
||||
} else if (response.status === 204) {
|
||||
// User not found, create a new user
|
||||
const kind0 = await fetchKind0(publicKey);
|
||||
const author = await ndk.getUser({ pubkey: publicKey });
|
||||
const kind0 = await author.fetchProfile();
|
||||
|
||||
console.log('kind0:', kind0);
|
||||
|
||||
@ -57,7 +58,7 @@ export const useLogin = () => {
|
||||
};
|
||||
|
||||
autoLogin();
|
||||
}, []);
|
||||
}, [ndk, showToast]);
|
||||
|
||||
const nostrLogin = useCallback(async () => {
|
||||
if (!window || !window.nostr) {
|
||||
@ -77,7 +78,8 @@ export const useLogin = () => {
|
||||
|
||||
if (response.status === 204) {
|
||||
// User not found, create a new user
|
||||
const kind0 = await fetchKind0(publicKey);
|
||||
const author = await ndk.getUser({ pubkey: publicKey });
|
||||
const kind0 = await author.fetchProfile();
|
||||
|
||||
let fields = {};
|
||||
if (kind0) {
|
||||
@ -100,7 +102,7 @@ export const useLogin = () => {
|
||||
console.error('Error during login:', error);
|
||||
showToast('error', 'Login Error', error.message || 'Failed to log in');
|
||||
}
|
||||
}, [router, showToast, fetchKind0]);
|
||||
}, [router, showToast, ndk]);
|
||||
|
||||
const anonymousLogin = useCallback(() => {
|
||||
try {
|
||||
|
@ -1,565 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useContext, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { nip57, nip19 } from 'nostr-tools';
|
||||
import { NostrContext } from '@/context/NostrContext';
|
||||
import { lnurlEncode } from '@/utils/lnurl';
|
||||
import { parseEvent } from '@/utils/nostr';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const defaultRelays = [
|
||||
"wss://nos.lol/",
|
||||
"wss://relay.damus.io/",
|
||||
"wss://relay.snort.social/",
|
||||
"wss://relay.nostr.band/",
|
||||
"wss://nostr.mutinywallet.com/",
|
||||
"wss://relay.mutinywallet.com/",
|
||||
"wss://relay.primal.net/"
|
||||
];
|
||||
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||
|
||||
export function useNostr() {
|
||||
const pool = useContext(NostrContext);
|
||||
const subscriptionQueue = useRef([]);
|
||||
const lastSubscriptionTime = useRef(0);
|
||||
const throttleDelay = 2000;
|
||||
|
||||
const processSubscriptionQueue = useCallback(() => {
|
||||
if (subscriptionQueue.current.length === 0) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastSubscriptionTime.current < throttleDelay) {
|
||||
setTimeout(processSubscriptionQueue, throttleDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = subscriptionQueue.current.shift();
|
||||
subscription();
|
||||
|
||||
lastSubscriptionTime.current = currentTime;
|
||||
setTimeout(processSubscriptionQueue, throttleDelay);
|
||||
}, [throttleDelay]);
|
||||
|
||||
const subscribe = useCallback(
|
||||
(filters, opts) => {
|
||||
if (!pool) return;
|
||||
|
||||
const subscriptionFn = () => {
|
||||
return pool.subscribeMany(defaultRelays, filters, opts);
|
||||
};
|
||||
|
||||
subscriptionQueue.current.push(subscriptionFn);
|
||||
processSubscriptionQueue();
|
||||
},
|
||||
[pool, processSubscriptionQueue]
|
||||
);
|
||||
|
||||
const publish = useCallback(
|
||||
async (event) => {
|
||||
if (!pool) return;
|
||||
|
||||
try {
|
||||
await Promise.any(pool.publish(defaultRelays, event));
|
||||
console.log('Published event to at least one relay');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to publish event:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[pool]
|
||||
);
|
||||
|
||||
const fetchSingleEvent = useCallback(
|
||||
async (id) => {
|
||||
try {
|
||||
const event = await new Promise((resolve, reject) => {
|
||||
subscribe(
|
||||
[{ ids: [id] }],
|
||||
{
|
||||
onevent: (event) => {
|
||||
console.log('Fetched event:', event);
|
||||
resolve(event);
|
||||
},
|
||||
onerror: (error) => {
|
||||
console.error('Failed to fetch event:', error);
|
||||
reject(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
return event;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch event:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[subscribe]
|
||||
);
|
||||
|
||||
const fetchSingleNaddrEvent = useCallback(
|
||||
async (id) => {
|
||||
try {
|
||||
const event = await new Promise((resolve, reject) => {
|
||||
subscribe(
|
||||
[{ "#d": [id] }],
|
||||
{
|
||||
onevent: (event) => {
|
||||
resolve(event);
|
||||
},
|
||||
onerror: (error) => {
|
||||
console.error('Failed to fetch event:', error);
|
||||
reject(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
return event;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch event:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[subscribe]
|
||||
);
|
||||
|
||||
const querySyncQueue = useRef([]);
|
||||
const lastQuerySyncTime = useRef(0);
|
||||
|
||||
const processQuerySyncQueue = useCallback(() => {
|
||||
if (querySyncQueue.current.length === 0) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastQuerySyncTime.current < throttleDelay) {
|
||||
setTimeout(processQuerySyncQueue, throttleDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
const querySync = querySyncQueue.current.shift();
|
||||
querySync();
|
||||
|
||||
lastQuerySyncTime.current = currentTime;
|
||||
setTimeout(processQuerySyncQueue, throttleDelay);
|
||||
}, [throttleDelay]);
|
||||
|
||||
const fetchZapsForParamaterizedEvent = useCallback(
|
||||
async (kind, id, d) => {
|
||||
try {
|
||||
const filters = { kinds: [9735], '#a': [`${kind}:${id}:${d}`] };
|
||||
const zaps = await pool.querySync(defaultRelays, filters);
|
||||
return zaps;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch zaps for event:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[pool]
|
||||
);
|
||||
|
||||
const fetchZapsForNonParameterizedEvent = useCallback(
|
||||
async (id) => {
|
||||
try {
|
||||
const filters = { kinds: [9735], '#e': [id] };
|
||||
const zaps = await pool.querySync(defaultRelays, filters);
|
||||
return zaps;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch zaps for event:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[pool]
|
||||
);
|
||||
|
||||
const fetchZapsForEvent = useCallback(
|
||||
async (event) => {
|
||||
const querySyncFn = async () => {
|
||||
try {
|
||||
const parameterizedZaps = await fetchZapsForParamaterizedEvent(event.kind, event.id, event.d);
|
||||
const nonParameterizedZaps = await fetchZapsForNonParameterizedEvent(event.id);
|
||||
return [...parameterizedZaps, ...nonParameterizedZaps];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch zaps for event:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
querySyncQueue.current.push(async () => {
|
||||
const zaps = await querySyncFn();
|
||||
resolve(zaps);
|
||||
});
|
||||
processQuerySyncQueue();
|
||||
});
|
||||
},
|
||||
[fetchZapsForParamaterizedEvent, fetchZapsForNonParameterizedEvent, processQuerySyncQueue]
|
||||
);
|
||||
|
||||
const fetchZapsForEvents = useCallback(
|
||||
async (events) => {
|
||||
const querySyncFn = async () => {
|
||||
try {
|
||||
// Collect all #a and #e tag values from the list of events
|
||||
let aTags = [];
|
||||
let aTagsAlt = [];
|
||||
let eTags = [];
|
||||
events.forEach(event => {
|
||||
aTags.push(`${event.kind}:${event.id}:${event.d}`);
|
||||
aTagsAlt.push(`${event.kind}:${event.pubkey}:${event.d}`);
|
||||
eTags.push(event.id);
|
||||
});
|
||||
|
||||
// Create filters for batch querying
|
||||
const filterA = { kinds: [9735], '#a': aTags };
|
||||
const filterE = { kinds: [9735], '#e': eTags };
|
||||
const filterAAlt = { kinds: [9735], '#a': aTagsAlt };
|
||||
|
||||
// Perform batch queries
|
||||
// const [zapsA, zapsE] = await Promise.all([
|
||||
// pool.querySync(defaultRelays, filterA),
|
||||
// pool.querySync(defaultRelays, filterE)
|
||||
// ]);
|
||||
let allZaps = []
|
||||
|
||||
await new Promise((resolve) => pool.subscribeMany(defaultRelays, [filterA, filterE, filterAAlt], {
|
||||
onerror: (error) => {
|
||||
console.error('Failed to fetch zaps for events:', error);
|
||||
resolve([]);
|
||||
},
|
||||
onevent: (event) => {
|
||||
allZaps.push(event);
|
||||
},
|
||||
oneose: () => {
|
||||
resolve(allZaps);
|
||||
}
|
||||
}))
|
||||
|
||||
// remove any duplicates
|
||||
allZaps = allZaps.filter((zap, index, self) => index === self.findIndex((t) => (
|
||||
t.id === zap.id
|
||||
)))
|
||||
|
||||
return allZaps;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch zaps for events:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
querySyncQueue.current.push(async () => {
|
||||
const zaps = await querySyncFn();
|
||||
resolve(zaps);
|
||||
});
|
||||
processQuerySyncQueue();
|
||||
});
|
||||
},
|
||||
[pool, processQuerySyncQueue]
|
||||
);
|
||||
|
||||
const fetchKind0 = useCallback(
|
||||
async (publicKey) => {
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve(null); // Resolve with null if no event is received within the timeout
|
||||
}, 10000); // 10 seconds timeout
|
||||
|
||||
subscribe(
|
||||
[{ authors: [publicKey], kinds: [0] }],
|
||||
{
|
||||
onevent: (event) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(JSON.parse(event.content));
|
||||
},
|
||||
onerror: (error) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('Error fetching kind 0:', error);
|
||||
resolve(null);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
[subscribe]
|
||||
);
|
||||
|
||||
const zapEvent = useCallback(
|
||||
async (event, amount, comment) => {
|
||||
const kind0 = await fetchKind0(event.pubkey);
|
||||
|
||||
if (kind0.length === 0) {
|
||||
console.error('Error fetching kind0');
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind0.lud16) {
|
||||
const lud16Username = kind0.lud16.split('@')[0];
|
||||
const lud16Domain = kind0.lud16.split('@')[1];
|
||||
const lud16Url = `https://${lud16Domain}/.well-known/lnurlp/${lud16Username}`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(lud16Url);
|
||||
|
||||
if (response.data.allowsNostr) {
|
||||
// const zapReq = nip57.makeZapRequest({
|
||||
// profile: event.pubkey,
|
||||
// event: event.id,
|
||||
// amount: amount,
|
||||
// relays: defaultRelays,
|
||||
// comment: comment ? comment : 'Plebdevs Zap',
|
||||
// });
|
||||
|
||||
const user = window.localStorage.getItem('user');
|
||||
|
||||
const pubkey = JSON.parse(user).pubkey;
|
||||
|
||||
const lnurl = lnurlEncode(lud16Url)
|
||||
|
||||
console.log('lnurl:', lnurl);
|
||||
|
||||
console.log('pubkey:', pubkey);
|
||||
|
||||
const zapReq = {
|
||||
kind: 9734,
|
||||
content: "",
|
||||
tags: [
|
||||
["relays", defaultRelays.join(",")],
|
||||
["amount", amount.toString()],
|
||||
// ["lnurl", lnurl],
|
||||
["e", event.id],
|
||||
["p", event.pubkey],
|
||||
["a", `${event.kind}:${event.pubkey}:${event.d}`],
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log('zapRequest:', zapReq);
|
||||
|
||||
const signedEvent = await window?.nostr?.signEvent(zapReq);
|
||||
console.log('signedEvent:', signedEvent);
|
||||
const callbackUrl = response.data.callback;
|
||||
const zapRequestAPICall = `${callbackUrl}?amount=${amount}&nostr=${encodeURI(
|
||||
JSON.stringify(signedEvent)
|
||||
)}`;
|
||||
|
||||
const invoiceResponse = await axios.get(zapRequestAPICall);
|
||||
|
||||
if (invoiceResponse?.data?.pr) {
|
||||
const invoice = invoiceResponse.data.pr;
|
||||
const enabled = await window?.webln?.enable();
|
||||
console.log('webln enabled:', enabled);
|
||||
const payInvoiceResponse = await window?.webln?.sendPayment(invoice);
|
||||
console.log('payInvoiceResponse:', payInvoiceResponse);
|
||||
} else {
|
||||
console.error('Error fetching invoice');
|
||||
// showToast('error', 'Error', 'Error fetching invoice');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching lud16 data:', error);
|
||||
}
|
||||
} else if (profile.lud06) {
|
||||
// handle lnurlpay
|
||||
} else {
|
||||
showToast('error', 'Error', 'User has no Lightning Address or LNURL');
|
||||
}
|
||||
},
|
||||
[fetchKind0]
|
||||
);
|
||||
|
||||
const fetchResources = useCallback(async () => {
|
||||
const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
|
||||
const hasRequiredTags = (tags) => {
|
||||
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||
const hasResource = tags.some(([tag, value]) => tag === "t" && value === "resource");
|
||||
return hasPlebDevs && hasResource;
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resources = [];
|
||||
const subscription = subscribe(
|
||||
filter,
|
||||
{
|
||||
onevent: (event) => {
|
||||
if (hasRequiredTags(event.tags)) {
|
||||
resources.push(event);
|
||||
}
|
||||
},
|
||||
onerror: (error) => {
|
||||
console.error('Error fetching resources:', error);
|
||||
// Don't resolve here, just log the error
|
||||
},
|
||||
onclose: () => {
|
||||
// Don't resolve here either
|
||||
},
|
||||
},
|
||||
2000 // Adjust the timeout value as needed
|
||||
);
|
||||
|
||||
// Set a timeout to resolve the promise after collecting events
|
||||
setTimeout(() => {
|
||||
subscription?.close();
|
||||
resolve(resources);
|
||||
}, 2000); // Adjust the timeout value as needed
|
||||
});
|
||||
}, [subscribe]);
|
||||
|
||||
const fetchWorkshops = useCallback(async () => {
|
||||
const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }];
|
||||
const hasRequiredTags = (tags) => {
|
||||
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop");
|
||||
return hasPlebDevs && hasWorkshop;
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let workshops = [];
|
||||
const subscription = subscribe(
|
||||
filter,
|
||||
{
|
||||
onevent: (event) => {
|
||||
if (hasRequiredTags(event.tags)) {
|
||||
workshops.push(event);
|
||||
}
|
||||
},
|
||||
onerror: (error) => {
|
||||
console.error('Error fetching workshops:', error);
|
||||
// Don't resolve here, just log the error
|
||||
},
|
||||
onclose: () => {
|
||||
// Don't resolve here either
|
||||
},
|
||||
},
|
||||
2000 // Adjust the timeout value as needed
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
subscription?.close();
|
||||
resolve(workshops);
|
||||
}, 2000); // Adjust the timeout value as needed
|
||||
});
|
||||
}, [subscribe]);
|
||||
|
||||
const fetchCourses = useCallback(async () => {
|
||||
const filter = [{ kinds: [30004], authors: [AUTHOR_PUBKEY] }];
|
||||
// Do we need required tags for courses? community instead?
|
||||
// const hasRequiredTags = (tags) => {
|
||||
// const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||
// const hasCourse = tags.some(([tag, value]) => tag === "t" && value === "course");
|
||||
// return hasPlebDevs && hasCourse;
|
||||
// };
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let courses = [];
|
||||
const subscription = subscribe(
|
||||
filter,
|
||||
{
|
||||
onevent: (event) => {
|
||||
// if (hasRequiredTags(event.tags)) {
|
||||
// courses.push(event);
|
||||
// }
|
||||
courses.push(event);
|
||||
},
|
||||
onerror: (error) => {
|
||||
console.error('Error fetching courses:', error);
|
||||
// Don't resolve here, just log the error
|
||||
},
|
||||
onclose: () => {
|
||||
// Don't resolve here either
|
||||
},
|
||||
},
|
||||
2000 // Adjust the timeout value as needed
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
subscription?.close();
|
||||
resolve(courses);
|
||||
}, 2000); // Adjust the timeout value as needed
|
||||
});
|
||||
}, [subscribe]);
|
||||
|
||||
const publishResource = useCallback(
|
||||
async (resourceEvent) => {
|
||||
const published = await publish(resourceEvent);
|
||||
|
||||
if (published) {
|
||||
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resourceEvent);
|
||||
|
||||
const user = window.localStorage.getItem('user');
|
||||
const userId = JSON.parse(user).id;
|
||||
|
||||
const payload = {
|
||||
id: uuidv4(),
|
||||
user: {
|
||||
connect: { id: userId } // This is the correct way to connect to an existing user
|
||||
},
|
||||
noteId: id
|
||||
};
|
||||
|
||||
if (payload && payload.user) {
|
||||
try {
|
||||
const response = await axios.post('/api/resources', payload);
|
||||
|
||||
if (response.status === 201) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating resource:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[publish]
|
||||
);
|
||||
|
||||
|
||||
const publishCourse = useCallback(
|
||||
async (courseEvent) => {
|
||||
const published = await publish(courseEvent);
|
||||
|
||||
if (published) {
|
||||
const user = window.localStorage.getItem('user');
|
||||
const pubkey = JSON.parse(user).pubkey;
|
||||
|
||||
const payload = {
|
||||
title: courseEvent.title,
|
||||
summary: courseEvent.summary,
|
||||
type: 'course',
|
||||
content: courseEvent.content,
|
||||
image: courseEvent.image,
|
||||
user: pubkey,
|
||||
topics: [...courseEvent.topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'course']
|
||||
};
|
||||
|
||||
if (payload && payload.user) {
|
||||
try {
|
||||
const response = await axios.post('/api/courses', payload);
|
||||
|
||||
if (response.status === 201) {
|
||||
try {
|
||||
const deleteResponse = await axios.delete(`/api/drafts/${courseEvent.id}`);
|
||||
|
||||
if (deleteResponse.status === 204) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting draft:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating course:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[publish]
|
||||
);
|
||||
|
||||
return { subscribe, publish, fetchSingleEvent, fetchSingleNaddrEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse };
|
||||
}
|
@ -8,7 +8,6 @@ import 'primereact/resources/themes/lara-dark-indigo/theme.css';
|
||||
import "@uiw/react-md-editor/markdown-editor.css";
|
||||
import "@uiw/react-markdown-preview/markdown.css";
|
||||
import Sidebar from '@/components/sidebar/Sidebar';
|
||||
import { NostrProvider } from '@/context/NostrContext';
|
||||
import { NDKProvider } from '@/context/NDKContext';
|
||||
import {
|
||||
QueryClient,
|
||||
@ -22,26 +21,24 @@ export default function MyApp({
|
||||
}) {
|
||||
return (
|
||||
<PrimeReactProvider>
|
||||
<NostrProvider>
|
||||
<NDKProvider>
|
||||
<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> */}
|
||||
<NDKProvider>
|
||||
<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>
|
||||
</Layout>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</NDKProvider>
|
||||
</NostrProvider>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</Layout>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</NDKProvider>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useNostr } from '@/hooks/useNostr';
|
||||
import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr';
|
||||
import { verifyEvent, nip19, nip04 } from 'nostr-tools';
|
||||
import { hexToNpub } from '@/utils/nostr';
|
||||
import { nip19, nip04 } from 'nostr-tools';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
|
||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||
import { Button } from 'primereact/button';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { useNDKContext } from '@/context/NDKContext';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import Image from 'next/image';
|
||||
import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions';
|
||||
import 'primeicons/primeicons.css';
|
||||
@ -21,20 +22,33 @@ const MDDisplay = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
export default function Details() {
|
||||
function validateEvent(event) {
|
||||
if (typeof event.kind !== "number") return "Invalid kind";
|
||||
if (typeof event.content !== "string") return "Invalid content";
|
||||
if (typeof event.created_at !== "number") return "Invalid created_at";
|
||||
if (typeof event.pubkey !== "string") return "Invalid pubkey";
|
||||
if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return "Invalid pubkey format";
|
||||
|
||||
if (!Array.isArray(event.tags)) return "Invalid tags";
|
||||
for (let i = 0; i < event.tags.length; i++) {
|
||||
const tag = event.tags[i];
|
||||
if (!Array.isArray(tag)) return "Invalid tag structure";
|
||||
for (let j = 0; j < tag.length; j++) {
|
||||
if (typeof tag[j] === "object") return "Invalid tag value";
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function Draft() {
|
||||
const [draft, setDraft] = useState(null);
|
||||
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
const { publishCourse, publishResource, fetchSingleEvent } = useNostr();
|
||||
|
||||
const [user] = useLocalStorageWithEffect('user', {});
|
||||
|
||||
const { width, height } = useResponsiveImageDimensions();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { showToast } = useToast();
|
||||
const ndk = useNDKContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
@ -52,32 +66,90 @@ export default function Details() {
|
||||
}, [router.isReady, router.query]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (draft) {
|
||||
const { unsignedEvent, type } = await buildEvent(draft);
|
||||
try {
|
||||
if (draft) {
|
||||
const { unsignedEvent, type } = await buildEvent(draft);
|
||||
|
||||
if (unsignedEvent) {
|
||||
const published = await publishEvent(unsignedEvent, type);
|
||||
console.log('published:', published);
|
||||
// if successful, delete the draft, redirect to profile
|
||||
if (published) {
|
||||
axios.delete(`/api/drafts/${draft.id}`)
|
||||
.then(res => {
|
||||
if (res.status === 204) {
|
||||
showToast('success', 'Success', 'Draft deleted successfully.');
|
||||
router.push(`/profile`);
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to delete draft.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
const validationResult = validateEvent(unsignedEvent);
|
||||
if (validationResult !== true) {
|
||||
console.error('Invalid event:', validationResult);
|
||||
showToast('error', 'Error', `Invalid event: ${validationResult}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('unsignedEvent:', unsignedEvent.validate());
|
||||
console.log('unsignedEvent validation:', validationResult);
|
||||
|
||||
if (unsignedEvent) {
|
||||
const published = await unsignedEvent.publish();
|
||||
|
||||
const saved = await handlePostResource(unsignedEvent);
|
||||
// if successful, delete the draft, redirect to profile
|
||||
if (published && saved) {
|
||||
axios.delete(`/api/drafts/${draft.id}`)
|
||||
.then(res => {
|
||||
if (res.status === 204) {
|
||||
showToast('success', 'Success', 'Draft deleted successfully.');
|
||||
router.push(`/profile`);
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to delete draft.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to broadcast resource. Please try again.');
|
||||
}
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to broadcast resource. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('error', 'Failed to publish resource.', err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePostResource = async (resource) => {
|
||||
console.log('resourceeeeee:', resource.tags);
|
||||
const dTag = resource.tags.find(tag => tag[0] === 'd')[1];
|
||||
let price
|
||||
|
||||
try {
|
||||
price = resource.tags.find(tag => tag[0] === 'price')[1];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
price = 0;
|
||||
}
|
||||
|
||||
const nAddress = nip19.naddrEncode({
|
||||
pubkey: resource.pubkey,
|
||||
kind: resource.kind,
|
||||
identifier: dTag,
|
||||
});
|
||||
|
||||
const userResponse = await axios.get(`/api/users/${user.pubkey}`);
|
||||
|
||||
if (!userResponse.data) {
|
||||
showToast('error', 'Error', 'User not found', 'Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: dTag,
|
||||
userId: userResponse.data.id,
|
||||
price: Number(price),
|
||||
noteId: nAddress,
|
||||
};
|
||||
|
||||
const response = await axios.post(`/api/resources`, payload);
|
||||
|
||||
if (response.status !== 201) {
|
||||
showToast('error', 'Error', 'Failed to create resource. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (draft) {
|
||||
@ -94,107 +166,38 @@ export default function Details() {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const publishEvent = async (event, type) => {
|
||||
const dTag = event.tags.find(tag => tag[0] === 'd')[1];
|
||||
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
const eventVerification = await verifyEvent(signedEvent);
|
||||
|
||||
if (!eventVerification) {
|
||||
showToast('error', 'Error', 'Event verification failed. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const nAddress = nip19.naddrEncode({
|
||||
pubkey: signedEvent.pubkey,
|
||||
kind: signedEvent.kind,
|
||||
identifier: dTag,
|
||||
})
|
||||
|
||||
const userResponse = await axios.get(`/api/users/${user.pubkey}`)
|
||||
|
||||
if (!userResponse.data) {
|
||||
showToast('error', 'Error', 'User not found', 'Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: dTag,
|
||||
userId: userResponse.data.id,
|
||||
price: Number(draft.price) || 0,
|
||||
noteId: nAddress,
|
||||
}
|
||||
|
||||
const response = await axios.post(`/api/resources`, payload);
|
||||
|
||||
if (response.status !== 201) {
|
||||
showToast('error', 'Error', 'Failed to create resource. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
let published;
|
||||
console.log('type:', type);
|
||||
|
||||
if (type === 'resource' || type === 'workshop') {
|
||||
published = await publishResource(signedEvent);
|
||||
} else if (type === 'course') {
|
||||
published = await publishCourse(signedEvent);
|
||||
}
|
||||
|
||||
if (published) {
|
||||
// check if the event is published
|
||||
const publishedEvent = await fetchSingleEvent(signedEvent.id);
|
||||
|
||||
if (publishedEvent) {
|
||||
// show success message
|
||||
showToast('success', 'Success', `${type} published successfully.`);
|
||||
// delete the draft
|
||||
await axios.delete(`/api/drafts/${draft.id}`)
|
||||
.then(res => {
|
||||
if (res.status === 204) {
|
||||
showToast('success', 'Success', 'Draft deleted successfully.');
|
||||
router.push(`/profile`);
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to delete draft.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildEvent = async (draft) => {
|
||||
const NewDTag = uuidv4();
|
||||
let event = {};
|
||||
const event = new NDKEvent(ndk);
|
||||
let type;
|
||||
let encryptedContent;
|
||||
|
||||
console.log('Draft:', draft);
|
||||
console.log('NewDTag:', NewDTag);
|
||||
|
||||
switch (draft?.type) {
|
||||
case 'resource':
|
||||
if (draft?.price) {
|
||||
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
||||
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
|
||||
}
|
||||
event = {
|
||||
kind: draft?.price ? 30402 : 30023, // Determine kind based on if price is present
|
||||
content: draft?.price ? encryptedContent : draft.content,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
// Include price and location tags only if price is present
|
||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||
]
|
||||
};
|
||||
|
||||
event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present
|
||||
event.content = draft?.price ? encryptedContent : draft.content;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = user.pubkey;
|
||||
event.tags = [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||
];
|
||||
|
||||
type = 'resource';
|
||||
break;
|
||||
case 'workshop':
|
||||
@ -202,40 +205,40 @@ export default function Details() {
|
||||
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
||||
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
|
||||
}
|
||||
event = {
|
||||
kind: draft?.price ? 30402 : 30023,
|
||||
content: draft?.price ? encryptedContent : draft.content,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
]
|
||||
};
|
||||
|
||||
event.kind = draft?.price ? 30402 : 30023;
|
||||
event.content = draft?.price ? encryptedContent : draft.content;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = user.pubkey;
|
||||
event.tags = [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
];
|
||||
|
||||
type = 'workshop';
|
||||
break;
|
||||
case 'course':
|
||||
event = {
|
||||
kind: 30023,
|
||||
content: draft.content,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
]
|
||||
};
|
||||
event.kind = 30023;
|
||||
event.content = draft.content;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = user.pubkey;
|
||||
event.tags = [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
];
|
||||
|
||||
type = 'course';
|
||||
break;
|
||||
default:
|
||||
event = null;
|
||||
type = 'unknown';
|
||||
return null;
|
||||
}
|
||||
|
||||
return { unsignedEvent: event, type };
|
||||
@ -244,7 +247,7 @@ export default function Details() {
|
||||
return (
|
||||
<div className='w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2'>
|
||||
<div className='w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col'>
|
||||
{/* <i className='pi pi-arrow-left pl-8 cursor-pointer hover:opacity-75 max-tab:pl-2 max-mob:pl-2' onClick={() => router.push('/')} /> */}
|
||||
<i className='pi pi-arrow-left pl-8 cursor-pointer hover:opacity-75 max-tab:pl-2 max-mob:pl-2' onClick={() => router.push('/')} />
|
||||
<div className='w-[75vw] mx-auto flex flex-row items-start justify-between max-tab:flex-col max-mob:flex-col max-tab:w-[95vw] max-mob:w-[95vw]'>
|
||||
<div className='flex flex-col items-start max-w-[45vw] max-tab:max-w-[100vw] max-mob:max-w-[100vw]'>
|
||||
<div className='pt-2 flex flex-row justify-start w-full'>
|
||||
@ -254,8 +257,7 @@ export default function Details() {
|
||||
return (
|
||||
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
||||
)
|
||||
})
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
||||
<p className='text-xl mt-6'>{draft?.summary}</p>
|
||||
@ -271,7 +273,7 @@ export default function Details() {
|
||||
<p className='text-lg'>
|
||||
Created by{' '}
|
||||
<a href={`https://nostr.com/${hexToNpub(user?.pubkey)}`} rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
|
||||
{user?.username || user?.pubkey.slice(0, 10)}{'... '}
|
||||
{user?.username || user?.name || user?.pubkey.slice(0, 10)}{'... '}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
@ -309,4 +311,4 @@ export default function Details() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ const Profile = () => {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const menu = useRef(null);
|
||||
|
||||
console.log('user:', user);
|
||||
|
||||
const purchases = [];
|
||||
|
||||
const menuItems = [
|
||||
|
Loading…
x
Reference in New Issue
Block a user