mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +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 author = await ndk.getUser({ pubkey });
|
||||||
const profile = await author.fetchProfile();
|
const profile = await author.fetchProfile();
|
||||||
const fields = await findKind0Fields(profile);
|
const fields = await findKind0Fields(profile);
|
||||||
console.log('fields:', fields);
|
|
||||||
if (fields) {
|
if (fields) {
|
||||||
setAuthor(fields);
|
setAuthor(fields);
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { InputText } from "primereact/inputtext";
|
import { InputText } from "primereact/inputtext";
|
||||||
import { InputNumber } from "primereact/inputnumber";
|
import { InputNumber } from "primereact/inputnumber";
|
||||||
import { InputSwitch } from "primereact/inputswitch";
|
import { InputSwitch } from "primereact/inputswitch";
|
||||||
import { Button } from "primereact/button";
|
import { Button } from "primereact/button";
|
||||||
import { Dropdown } from "primereact/dropdown";
|
import { Dropdown } from "primereact/dropdown";
|
||||||
|
import { ProgressSpinner } from "primereact/progressspinner";
|
||||||
import { v4 as uuidv4, v4 } from 'uuid';
|
import { v4 as uuidv4, v4 } from 'uuid';
|
||||||
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
||||||
import { useNostr } from "@/hooks/useNostr";
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useToast } from "@/hooks/useToast";
|
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 { parseEvent } from "@/utils/nostr";
|
||||||
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
|
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
@ -24,47 +28,15 @@ const CourseForm = () => {
|
|||||||
const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]);
|
const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]);
|
||||||
const [selectedLessons, setSelectedLessons] = useState([]);
|
const [selectedLessons, setSelectedLessons] = useState([]);
|
||||||
const [topics, setTopics] = 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 router = useRouter();
|
||||||
const { showToast } = useToast();
|
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:
|
* Course Creation Flow:
|
||||||
* 1. Generate a new course ID
|
* 1. Generate a new course ID
|
||||||
@ -90,14 +62,13 @@ const CourseForm = () => {
|
|||||||
if (!lesson.published_at) {
|
if (!lesson.published_at) {
|
||||||
// Publish unpublished lesson
|
// Publish unpublished lesson
|
||||||
const event = createLessonEvent(lesson);
|
const event = createLessonEvent(lesson);
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
const published = await event.publish();
|
||||||
const published = await publish(signedEvent);
|
|
||||||
|
|
||||||
if (!published) {
|
if (!published) {
|
||||||
throw new Error(`Failed to publish lesson: ${lesson.title}`);
|
throw new Error(`Failed to publish lesson: ${lesson.title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
noteId = signedEvent.id;
|
noteId = event.id;
|
||||||
|
|
||||||
// Save to db and delete draft
|
// Save to db and delete draft
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -122,8 +93,9 @@ const CourseForm = () => {
|
|||||||
|
|
||||||
// Step 2: Create and publish course
|
// Step 2: Create and publish course
|
||||||
const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, processedLessons);
|
const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, processedLessons);
|
||||||
const signedCourseEvent = await window.nostr.signEvent(courseEvent);
|
const published = await courseEvent.publish();
|
||||||
const published = await publish(signedCourseEvent);
|
|
||||||
|
console.log('published', published);
|
||||||
|
|
||||||
if (!published) {
|
if (!published) {
|
||||||
throw new Error('Failed to publish course');
|
throw new Error('Failed to publish course');
|
||||||
@ -136,7 +108,7 @@ const CourseForm = () => {
|
|||||||
resources: {
|
resources: {
|
||||||
connect: processedLessons.map(lesson => ({ id: lesson?.d }))
|
connect: processedLessons.map(lesson => ({ id: lesson?.d }))
|
||||||
},
|
},
|
||||||
noteId: signedCourseEvent.id,
|
noteId: courseEvent.id,
|
||||||
user: {
|
user: {
|
||||||
connect: { id: user.id }
|
connect: { id: user.id }
|
||||||
},
|
},
|
||||||
@ -148,7 +120,7 @@ const CourseForm = () => {
|
|||||||
|
|
||||||
// Step 5: Show success message and redirect
|
// Step 5: Show success message and redirect
|
||||||
showToast('success', 'Course created successfully');
|
showToast('success', 'Course created successfully');
|
||||||
router.push(`/course/${signedCourseEvent.id}`);
|
router.push(`/course/${courseEvent.id}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating course:', error);
|
console.error('Error creating course:', error);
|
||||||
@ -156,29 +128,26 @@ const CourseForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createLessonEvent = (lesson) => ({
|
const createLessonEvent = (lesson) => {
|
||||||
kind: lesson.price ? 30402 : 30023,
|
const event = new NDKEvent(ndk);
|
||||||
content: lesson.content,
|
event.kind = lesson.price ? 30402 : 30023;
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
event.content = lesson.content;
|
||||||
tags: [
|
event.tags = [
|
||||||
['d', lesson.id],
|
['d', lesson.id],
|
||||||
['title', lesson.title],
|
['title', lesson.title],
|
||||||
['summary', lesson.summary],
|
['summary', lesson.summary],
|
||||||
['image', lesson.image],
|
['image', lesson.image],
|
||||||
...lesson.topics.map(topic => ['t', topic]),
|
...lesson.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
...(lesson.price ? [
|
];
|
||||||
['price', lesson.price],
|
return event;
|
||||||
['location', `https://plebdevs.com/${lesson.topics[1]}/${lesson.id}`]
|
};
|
||||||
] : [])
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const createCourseEvent = (courseId, title, summary, coverImage, lessons) => ({
|
const createCourseEvent = (courseId, title, summary, coverImage, lessons) => {
|
||||||
kind: 30004,
|
const event = new NDKEvent(ndk);
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
event.kind = 30004;
|
||||||
content: "",
|
event.content = "";
|
||||||
tags: [
|
event.tags = [
|
||||||
['d', courseId],
|
['d', courseId],
|
||||||
['name', title],
|
['name', title],
|
||||||
['picture', coverImage],
|
['picture', coverImage],
|
||||||
@ -186,8 +155,9 @@ const CourseForm = () => {
|
|||||||
['description', summary],
|
['description', summary],
|
||||||
['l', "Education"],
|
['l', "Education"],
|
||||||
...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]),
|
...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]),
|
||||||
],
|
];
|
||||||
});
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
const handleLessonChange = (e, index) => {
|
const handleLessonChange = (e, index) => {
|
||||||
const selectedLessonId = e.value;
|
const selectedLessonId = e.value;
|
||||||
@ -235,6 +205,9 @@ const CourseForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getContentOptions = (index) => {
|
const getContentOptions = (index) => {
|
||||||
|
if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const draftOptions = drafts.map(draft => ({
|
const draftOptions = drafts.map(draft => ({
|
||||||
label: <ContentDropdownItem content={draft} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />,
|
label: <ContentDropdownItem content={draft} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />,
|
||||||
value: draft.id
|
value: draft.id
|
||||||
@ -272,7 +245,10 @@ const CourseForm = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const lessonOptions = getContentOptions();
|
// const lessonOptions = getContentOptions();
|
||||||
|
if (resourcesLoading || workshopsLoading || draftsLoading) {
|
||||||
|
return <ProgressSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<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 { InputNumber } from "primereact/inputnumber";
|
||||||
import { InputSwitch } from "primereact/inputswitch";
|
import { InputSwitch } from "primereact/inputswitch";
|
||||||
import { Button } from "primereact/button";
|
import { Button } from "primereact/button";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";;
|
||||||
import { useNostr } from "@/hooks/useNostr";
|
|
||||||
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
|
||||||
import EditorHeader from "./Editor/EditorHeader";
|
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
const MDEditor = dynamic(
|
const MDEditor = dynamic(
|
||||||
@ -29,7 +27,6 @@ const ResourceForm = ({ draft = null }) => {
|
|||||||
|
|
||||||
const [user] = useLocalStorageWithEffect('user', {});
|
const [user] = useLocalStorageWithEffect('user', {});
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { publishAll } = useNostr();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleContentChange = useCallback((value) => {
|
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 handleTopicChange = (index, value) => {
|
||||||
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
||||||
setTopics(updatedTopics);
|
setTopics(updatedTopics);
|
||||||
@ -255,39 +107,6 @@ const ResourceForm = ({ draft = null }) => {
|
|||||||
setTopics(updatedTopics);
|
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 (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="p-inputgroup flex-1">
|
<div className="p-inputgroup flex-1">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
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);
|
const NDKContext = createContext(null);
|
||||||
|
|
||||||
@ -17,7 +17,8 @@ export const NDKProvider = ({ children }) => {
|
|||||||
const [ndk, setNdk] = useState(null);
|
const [ndk, setNdk] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const instance = new NDK({ explicitRelayUrls: relayUrls });
|
const nip07signer = new NDKNip07Signer();
|
||||||
|
const instance = new NDK({ explicitRelayUrls: relayUrls, signer: nip07signer });
|
||||||
setNdk(instance);
|
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 { useCallback, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useNostr } from './useNostr';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
import { findKind0Fields } from "@/utils/nostr";
|
import { findKind0Fields } from "@/utils/nostr";
|
||||||
import { useToast } from './useToast';
|
import { useToast } from './useToast';
|
||||||
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { fetchKind0 } = useNostr();
|
const ndk = useNDKContext();
|
||||||
|
|
||||||
// Attempt Auto Login on render
|
// Attempt Auto Login on render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -26,7 +26,8 @@ export const useLogin = () => {
|
|||||||
window.localStorage.setItem('user', JSON.stringify(response.data));
|
window.localStorage.setItem('user', JSON.stringify(response.data));
|
||||||
} else if (response.status === 204) {
|
} else if (response.status === 204) {
|
||||||
// User not found, create a new user
|
// 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);
|
console.log('kind0:', kind0);
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ export const useLogin = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
autoLogin();
|
autoLogin();
|
||||||
}, []);
|
}, [ndk, showToast]);
|
||||||
|
|
||||||
const nostrLogin = useCallback(async () => {
|
const nostrLogin = useCallback(async () => {
|
||||||
if (!window || !window.nostr) {
|
if (!window || !window.nostr) {
|
||||||
@ -77,7 +78,8 @@ export const useLogin = () => {
|
|||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
// User not found, create a new user
|
// 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 = {};
|
let fields = {};
|
||||||
if (kind0) {
|
if (kind0) {
|
||||||
@ -100,7 +102,7 @@ export const useLogin = () => {
|
|||||||
console.error('Error during login:', error);
|
console.error('Error during login:', error);
|
||||||
showToast('error', 'Login Error', error.message || 'Failed to log in');
|
showToast('error', 'Login Error', error.message || 'Failed to log in');
|
||||||
}
|
}
|
||||||
}, [router, showToast, fetchKind0]);
|
}, [router, showToast, ndk]);
|
||||||
|
|
||||||
const anonymousLogin = useCallback(() => {
|
const anonymousLogin = useCallback(() => {
|
||||||
try {
|
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-md-editor/markdown-editor.css";
|
||||||
import "@uiw/react-markdown-preview/markdown.css";
|
import "@uiw/react-markdown-preview/markdown.css";
|
||||||
import Sidebar from '@/components/sidebar/Sidebar';
|
import Sidebar from '@/components/sidebar/Sidebar';
|
||||||
import { NostrProvider } from '@/context/NostrContext';
|
|
||||||
import { NDKProvider } from '@/context/NDKContext';
|
import { NDKProvider } from '@/context/NDKContext';
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
@ -22,7 +21,6 @@ export default function MyApp({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<PrimeReactProvider>
|
<PrimeReactProvider>
|
||||||
<NostrProvider>
|
|
||||||
<NDKProvider>
|
<NDKProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
@ -41,7 +39,6 @@ export default function MyApp({
|
|||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</NDKProvider>
|
</NDKProvider>
|
||||||
</NostrProvider>
|
|
||||||
</PrimeReactProvider>
|
</PrimeReactProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,15 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useNostr } from '@/hooks/useNostr';
|
import { hexToNpub } from '@/utils/nostr';
|
||||||
import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr';
|
import { nip19, nip04 } from 'nostr-tools';
|
||||||
import { verifyEvent, nip19, nip04 } from 'nostr-tools';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
|
import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage';
|
||||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { Tag } from 'primereact/tag';
|
import { Tag } from 'primereact/tag';
|
||||||
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions';
|
import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions';
|
||||||
import 'primeicons/primeicons.css';
|
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 [draft, setDraft] = useState(null);
|
||||||
|
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
|
||||||
const { publishCourse, publishResource, fetchSingleEvent } = useNostr();
|
|
||||||
|
|
||||||
const [user] = useLocalStorageWithEffect('user', {});
|
const [user] = useLocalStorageWithEffect('user', {});
|
||||||
|
|
||||||
const { width, height } = useResponsiveImageDimensions();
|
const { width, height } = useResponsiveImageDimensions();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const ndk = useNDKContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.isReady) {
|
if (router.isReady) {
|
||||||
@ -52,14 +66,26 @@ export default function Details() {
|
|||||||
}, [router.isReady, router.query]);
|
}, [router.isReady, router.query]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
if (draft) {
|
if (draft) {
|
||||||
const { unsignedEvent, type } = await buildEvent(draft);
|
const { unsignedEvent, type } = await buildEvent(draft);
|
||||||
|
|
||||||
|
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) {
|
if (unsignedEvent) {
|
||||||
const published = await publishEvent(unsignedEvent, type);
|
const published = await unsignedEvent.publish();
|
||||||
console.log('published:', published);
|
|
||||||
|
const saved = await handlePostResource(unsignedEvent);
|
||||||
// if successful, delete the draft, redirect to profile
|
// if successful, delete the draft, redirect to profile
|
||||||
if (published) {
|
if (published && saved) {
|
||||||
axios.delete(`/api/drafts/${draft.id}`)
|
axios.delete(`/api/drafts/${draft.id}`)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.status === 204) {
|
if (res.status === 204) {
|
||||||
@ -77,7 +103,53 @@ export default function Details() {
|
|||||||
showToast('error', 'Error', 'Failed to broadcast resource. Please try again.');
|
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 () => {
|
const handleDelete = async () => {
|
||||||
if (draft) {
|
if (draft) {
|
||||||
@ -94,107 +166,38 @@ export default function Details() {
|
|||||||
console.error(err);
|
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 buildEvent = async (draft) => {
|
||||||
const NewDTag = uuidv4();
|
const NewDTag = uuidv4();
|
||||||
let event = {};
|
const event = new NDKEvent(ndk);
|
||||||
let type;
|
let type;
|
||||||
let encryptedContent;
|
let encryptedContent;
|
||||||
|
|
||||||
|
console.log('Draft:', draft);
|
||||||
|
console.log('NewDTag:', NewDTag);
|
||||||
|
|
||||||
switch (draft?.type) {
|
switch (draft?.type) {
|
||||||
case 'resource':
|
case 'resource':
|
||||||
if (draft?.price) {
|
if (draft?.price) {
|
||||||
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
// 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);
|
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
|
event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present
|
||||||
content: draft?.price ? encryptedContent : draft.content,
|
event.content = draft?.price ? encryptedContent : draft.content;
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
event.created_at = Math.floor(Date.now() / 1000);
|
||||||
tags: [
|
event.pubkey = user.pubkey;
|
||||||
|
event.tags = [
|
||||||
['d', NewDTag],
|
['d', NewDTag],
|
||||||
['title', draft.title],
|
['title', draft.title],
|
||||||
['summary', draft.summary],
|
['summary', draft.summary],
|
||||||
['image', draft.image],
|
['image', draft.image],
|
||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['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}`]] : []),
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
]
|
];
|
||||||
};
|
|
||||||
type = 'resource';
|
type = 'resource';
|
||||||
break;
|
break;
|
||||||
case 'workshop':
|
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
|
// 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);
|
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,
|
event.kind = draft?.price ? 30402 : 30023;
|
||||||
content: draft?.price ? encryptedContent : draft.content,
|
event.content = draft?.price ? encryptedContent : draft.content;
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
event.created_at = Math.floor(Date.now() / 1000);
|
||||||
tags: [
|
event.pubkey = user.pubkey;
|
||||||
|
event.tags = [
|
||||||
['d', NewDTag],
|
['d', NewDTag],
|
||||||
['title', draft.title],
|
['title', draft.title],
|
||||||
['summary', draft.summary],
|
['summary', draft.summary],
|
||||||
['image', draft.image],
|
['image', draft.image],
|
||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
]
|
];
|
||||||
};
|
|
||||||
type = 'workshop';
|
type = 'workshop';
|
||||||
break;
|
break;
|
||||||
case 'course':
|
case 'course':
|
||||||
event = {
|
event.kind = 30023;
|
||||||
kind: 30023,
|
event.content = draft.content;
|
||||||
content: draft.content,
|
event.created_at = Math.floor(Date.now() / 1000);
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
event.pubkey = user.pubkey;
|
||||||
tags: [
|
event.tags = [
|
||||||
['d', NewDTag],
|
['d', NewDTag],
|
||||||
['title', draft.title],
|
['title', draft.title],
|
||||||
['summary', draft.summary],
|
['summary', draft.summary],
|
||||||
['image', draft.image],
|
['image', draft.image],
|
||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
]
|
];
|
||||||
};
|
|
||||||
type = 'course';
|
type = 'course';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
event = null;
|
return null;
|
||||||
type = 'unknown';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { unsignedEvent: event, type };
|
return { unsignedEvent: event, type };
|
||||||
@ -244,7 +247,7 @@ export default function Details() {
|
|||||||
return (
|
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 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'>
|
<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='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='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'>
|
<div className='pt-2 flex flex-row justify-start w-full'>
|
||||||
@ -254,8 +257,7 @@ export default function Details() {
|
|||||||
return (
|
return (
|
||||||
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
||||||
<p className='text-xl mt-6'>{draft?.summary}</p>
|
<p className='text-xl mt-6'>{draft?.summary}</p>
|
||||||
@ -271,7 +273,7 @@ export default function Details() {
|
|||||||
<p className='text-lg'>
|
<p className='text-lg'>
|
||||||
Created by{' '}
|
Created by{' '}
|
||||||
<a href={`https://nostr.com/${hexToNpub(user?.pubkey)}`} rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
|
<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>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -15,6 +15,8 @@ const Profile = () => {
|
|||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
const menu = useRef(null);
|
const menu = useRef(null);
|
||||||
|
|
||||||
|
console.log('user:', user);
|
||||||
|
|
||||||
const purchases = [];
|
const purchases = [];
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user