2024-03-25 13:39:32 -05:00
|
|
|
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';
|
2024-04-29 14:48:15 -05:00
|
|
|
import { verifyEvent, nip19, nip04 } from 'nostr-tools';
|
2024-03-25 13:39:32 -05:00
|
|
|
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 Image from 'next/image';
|
|
|
|
import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions';
|
|
|
|
import 'primeicons/primeicons.css';
|
|
|
|
|
|
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
|
import rehypeRaw from 'rehype-raw';
|
|
|
|
|
|
|
|
const MarkdownContent = ({ content }) => {
|
2024-07-20 12:40:53 -05:00
|
|
|
// Function to strip HTML tags
|
|
|
|
const stripHtml = (html) => {
|
|
|
|
let tmp = document.createElement("DIV");
|
|
|
|
tmp.innerHTML = html;
|
|
|
|
return tmp.textContent || tmp.innerText || "";
|
|
|
|
};
|
|
|
|
|
|
|
|
// Strip HTML tags from the content
|
|
|
|
const plainContent = stripHtml(content);
|
|
|
|
|
2024-03-25 13:39:32 -05:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<ReactMarkdown rehypePlugins={[rehypeRaw]} className='markdown-content'>
|
2024-07-20 12:40:53 -05:00
|
|
|
{plainContent}
|
2024-03-25 13:39:32 -05:00
|
|
|
</ReactMarkdown>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default function Details() {
|
|
|
|
const [draft, setDraft] = useState(null);
|
|
|
|
|
|
|
|
const { returnImageProxy } = useImageProxy();
|
2024-04-29 14:48:15 -05:00
|
|
|
|
2024-07-20 10:51:16 -05:00
|
|
|
const { publishCourse, publishResource, fetchSingleEvent } = useNostr();
|
2024-03-25 13:39:32 -05:00
|
|
|
|
|
|
|
const [user] = useLocalStorageWithEffect('user', {});
|
|
|
|
|
|
|
|
const { width, height } = useResponsiveImageDimensions();
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
2024-04-24 11:19:31 -05:00
|
|
|
const { showToast } = useToast();
|
2024-03-25 13:39:32 -05:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (router.isReady) {
|
|
|
|
const { slug } = router.query;
|
|
|
|
|
|
|
|
axios.get(`/api/drafts/${slug}`)
|
|
|
|
.then(res => {
|
|
|
|
console.log('res:', res.data);
|
|
|
|
setDraft(res.data);
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
console.error(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [router.isReady, router.query]);
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
if (draft) {
|
2024-04-29 14:48:15 -05:00
|
|
|
const { unsignedEvent, type } = await buildEvent(draft);
|
2024-03-25 13:39:32 -05:00
|
|
|
|
|
|
|
if (unsignedEvent) {
|
|
|
|
await publishEvent(unsignedEvent, type);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
showToast('error', 'Error', 'Failed to broadcast resource. Please try again.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
|
|
|
|
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 payload = {
|
|
|
|
id: dTag,
|
|
|
|
userId: userResponse.data.id,
|
|
|
|
price: 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;
|
|
|
|
}
|
|
|
|
|
2024-07-20 10:51:16 -05:00
|
|
|
let published;
|
2024-04-29 14:48:15 -05:00
|
|
|
|
2024-07-20 10:51:16 -05:00
|
|
|
if (type === 'resource' || type === 'workshop') {
|
|
|
|
published = await publishResource(signedEvent);
|
|
|
|
} else if (type === 'course') {
|
|
|
|
published = await publishCourse(signedEvent);
|
|
|
|
}
|
2024-04-29 14:48:15 -05:00
|
|
|
|
2024-07-20 10:51:16 -05:00
|
|
|
if (published) {
|
|
|
|
// check if the event is published
|
|
|
|
const publishedEvent = await fetchSingleEvent(signedEvent.id);
|
|
|
|
|
|
|
|
console.log('publishedEvent:', publishedEvent);
|
|
|
|
|
|
|
|
if (publishedEvent) {
|
|
|
|
// show success message
|
|
|
|
showToast('success', 'Success', `${type} published successfully.`);
|
|
|
|
// delete the draft
|
|
|
|
await axios.delete(`/api/drafts/${draft.id}`)
|
2024-04-29 14:48:15 -05:00
|
|
|
.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);
|
|
|
|
});
|
2024-07-20 10:51:16 -05:00
|
|
|
}
|
2024-04-29 14:48:15 -05:00
|
|
|
}
|
2024-03-25 13:39:32 -05:00
|
|
|
}
|
|
|
|
|
2024-04-29 14:48:15 -05:00
|
|
|
const buildEvent = async (draft) => {
|
2024-03-25 13:39:32 -05:00
|
|
|
const NewDTag = uuidv4();
|
|
|
|
let event = {};
|
|
|
|
let type;
|
2024-04-29 14:48:15 -05:00
|
|
|
let encryptedContent;
|
2024-04-24 11:19:31 -05:00
|
|
|
|
2024-03-25 13:39:32 -05:00
|
|
|
switch (draft?.type) {
|
|
|
|
case 'resource':
|
2024-04-29 14:48:15 -05:00
|
|
|
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);
|
|
|
|
}
|
2024-03-25 13:39:32 -05:00
|
|
|
event = {
|
|
|
|
kind: draft?.price ? 30402 : 30023, // Determine kind based on if price is present
|
2024-04-29 14:48:15 -05:00
|
|
|
content: draft?.price ? encryptedContent : draft.content,
|
2024-03-25 13:39:32 -05:00
|
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
|
|
tags: [
|
|
|
|
['d', NewDTag],
|
|
|
|
['title', draft.title],
|
|
|
|
['summary', draft.summary],
|
|
|
|
['image', draft.image],
|
2024-04-29 14:48:15 -05:00
|
|
|
...draft.topics.map(topic => ['t', topic]),
|
2024-03-25 13:39:32 -05:00
|
|
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
|
|
|
// Include price and location tags only if price is present
|
|
|
|
...(draft?.price ? [['price', draft.price], ['location', `https://plebdevs.com/resource/${draft.id}`]] : []),
|
|
|
|
]
|
|
|
|
};
|
|
|
|
type = 'resource';
|
|
|
|
break;
|
|
|
|
case 'workshop':
|
2024-04-29 14:48:15 -05:00
|
|
|
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);
|
|
|
|
}
|
2024-03-25 13:39:32 -05:00
|
|
|
event = {
|
2024-04-29 14:48:15 -05:00
|
|
|
kind: draft?.price ? 30402 : 30023,
|
|
|
|
content: draft?.price ? encryptedContent : draft.content,
|
2024-03-25 13:39:32 -05:00
|
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
|
|
tags: [
|
|
|
|
['d', NewDTag],
|
|
|
|
['title', draft.title],
|
|
|
|
['summary', draft.summary],
|
|
|
|
['image', draft.image],
|
2024-04-29 14:48:15 -05:00
|
|
|
...draft.topics.map(topic => ['t', topic]),
|
2024-03-25 13:39:32 -05:00
|
|
|
['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],
|
2024-04-29 14:48:15 -05:00
|
|
|
...draft.topics.map(topic => ['t', topic]),
|
2024-03-25 13:39:32 -05:00
|
|
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
|
|
|
]
|
|
|
|
};
|
|
|
|
type = 'course';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
event = null;
|
|
|
|
type = 'unknown';
|
|
|
|
}
|
2024-04-24 11:19:31 -05:00
|
|
|
|
2024-03-25 13:39:32 -05:00
|
|
|
return { unsignedEvent: event, type };
|
|
|
|
};
|
|
|
|
|
|
|
|
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('/')} /> */}
|
|
|
|
<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'>
|
2024-04-24 11:19:31 -05:00
|
|
|
{/* List out topics */}
|
|
|
|
{draft?.topics && draft.topics.map((topic, index) => {
|
|
|
|
if (topic === "plebdevs") return;
|
|
|
|
return (
|
|
|
|
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2024-03-25 13:39:32 -05:00
|
|
|
</div>
|
|
|
|
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
|
|
|
<p className='text-xl mt-6'>{draft?.summary}</p>
|
|
|
|
<div className='flex flex-row w-full mt-6 items-center'>
|
|
|
|
<Image
|
|
|
|
alt="resource thumbnail"
|
|
|
|
src={user?.avatar ? returnImageProxy(user.avatar) : `https://secure.gravatar.com/avatar/${user.pubkey}?s=90&d=identicon`}
|
|
|
|
width={50}
|
|
|
|
height={50}
|
|
|
|
className="rounded-full mr-4"
|
|
|
|
/>
|
2024-07-20 12:40:53 -05:00
|
|
|
{user && user?.pubkey && (
|
|
|
|
<p className='text-lg'>
|
2024-03-25 13:39:32 -05:00
|
|
|
Created by{' '}
|
2024-07-20 12:40:53 -05:00
|
|
|
<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)}{'... '}
|
2024-03-25 13:39:32 -05:00
|
|
|
</a>
|
|
|
|
</p>
|
2024-07-20 12:40:53 -05:00
|
|
|
)}
|
2024-03-25 13:39:32 -05:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
|
|
|
{draft && (
|
|
|
|
<div style={{ width: width < 768 ? "auto" : width }} onClick={() => router.push(`/details/${draft.id}`)} className="flex flex-col items-center mx-auto cursor-pointer rounded-md shadow-lg">
|
|
|
|
<div style={{ maxWidth: width, minWidth: width }} className="max-tab:h-auto max-mob:h-auto">
|
|
|
|
<Image
|
|
|
|
alt="resource thumbnail"
|
|
|
|
src={returnImageProxy(draft.image)}
|
|
|
|
quality={100}
|
|
|
|
width={width}
|
|
|
|
height={height}
|
|
|
|
className="w-full h-full object-cover object-center rounded-md"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
|
2024-07-20 12:40:53 -05:00
|
|
|
<div className='w-[15vw] flex flex-row justify-between'>
|
|
|
|
<Button onClick={() => router.push(`/draft/${draft?.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto my-2" />
|
|
|
|
<Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto my-2" />
|
|
|
|
</div>
|
2024-03-25 13:39:32 -05:00
|
|
|
</div>
|
2024-03-28 20:11:51 -05:00
|
|
|
<div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]'>
|
2024-03-25 13:39:32 -05:00
|
|
|
{
|
|
|
|
draft?.content && <MarkdownContent content={draft.content} />
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|