From aca8c6ee8279646f10617f5bd6f30b23932f4789 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Mon, 25 Mar 2024 13:39:32 -0500 Subject: [PATCH] Starting on drafts flow, updating forms, updating db, some small ui fixes --- .../migration.sql | 20 ++ prisma/schema.prisma | 17 ++ src/components/forms/CourseForm.js | 2 +- src/components/forms/ResourceForm.js | 285 ++++++++++-------- src/db/models/courseModels.js | 12 +- src/db/models/draftModels.js | 46 +++ src/db/prisma.js | 27 +- src/hooks/useNostr.js | 6 +- src/hooks/useResponsiveImageDimensions.js | 39 +++ src/pages/_document.js | 1 - src/pages/api/drafts/[slug].js | 40 +++ src/pages/api/drafts/all/[slug].js | 22 ++ src/pages/api/drafts/index.js | 20 ++ src/pages/draft/[slug].js | 239 +++++++++++++++ 14 files changed, 633 insertions(+), 143 deletions(-) rename prisma/migrations/{20240320231910_init => 20240325154103_init}/migration.sql (83%) create mode 100644 src/db/models/draftModels.js create mode 100644 src/hooks/useResponsiveImageDimensions.js create mode 100644 src/pages/api/drafts/[slug].js create mode 100644 src/pages/api/drafts/all/[slug].js create mode 100644 src/pages/api/drafts/index.js create mode 100644 src/pages/draft/[slug].js diff --git a/prisma/migrations/20240320231910_init/migration.sql b/prisma/migrations/20240325154103_init/migration.sql similarity index 83% rename from prisma/migrations/20240320231910_init/migration.sql rename to prisma/migrations/20240325154103_init/migration.sql index 1392cb2..d8532eb 100644 --- a/prisma/migrations/20240320231910_init/migration.sql +++ b/prisma/migrations/20240325154103_init/migration.sql @@ -57,6 +57,23 @@ CREATE TABLE "Resource" ( CONSTRAINT "Resource_pkey" PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "Draft" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "content" TEXT NOT NULL, + "image" TEXT, + "price" INTEGER DEFAULT 0, + "topics" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Draft_pkey" PRIMARY KEY ("id") +); + -- CreateIndex CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey"); @@ -89,3 +106,6 @@ ALTER TABLE "Resource" ADD CONSTRAINT "Resource_userId_fkey" FOREIGN KEY ("userI -- AddForeignKey ALTER TABLE "Resource" ADD CONSTRAINT "Resource_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6334e16..dea4080 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,6 +15,7 @@ model User { purchased Purchase[] courses Course[] // Relation field added for courses created by the user resources Resource[] // Relation field added for resources created by the user + drafts Draft[] // Relation field added for drafts created by the user role Role? @relation(fields: [roleId], references: [id]) roleId String? createdAt DateTime @default(now()) @@ -64,3 +65,19 @@ model Resource { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Draft { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id]) + type String + title String + summary String + content String + image String? + price Int? @default(0) + topics String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 517f06a..234801b 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -17,7 +17,7 @@ const CourseForm = () => { const [coverImage, setCoverImage] = useState(''); const [topics, setTopics] = useState(['']); - const showToast = useToast(); + const {showToast} = useToast(); const handleSubmit = (e) => { e.preventDefault(); diff --git a/src/components/forms/ResourceForm.js b/src/components/forms/ResourceForm.js index 2fb5e26..6889445 100644 --- a/src/components/forms/ResourceForm.js +++ b/src/components/forms/ResourceForm.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { use, useState } from "react"; import axios from "axios"; import { InputText } from "primereact/inputtext"; import { InputNumber } from "primereact/inputnumber"; @@ -6,6 +6,7 @@ import { InputSwitch } from "primereact/inputswitch"; import { Editor } from "primereact/editor"; import { Button } from "primereact/button"; import { nip04, verifyEvent, nip19 } from "nostr-tools"; +import { useRouter } from "next/router"; import { useNostr } from "@/hooks/useNostr"; import { v4 as uuidv4 } from 'uuid'; import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; @@ -16,7 +17,7 @@ import 'primeicons/primeicons.css'; const ResourceForm = () => { const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); - const [checked, setChecked] = useState(false); + const [isPaidResource, setIsPaidResource] = useState(false); const [price, setPrice] = useState(0); const [text, setText] = useState(''); const [coverImage, setCoverImage] = useState(''); @@ -28,168 +29,190 @@ const ResourceForm = () => { const { publishAll } = useNostr(); - const handleSubmit = (e) => { + const router = useRouter(); + + const handleSubmit = async (e) => { e.preventDefault(); + + const userResponse = await axios.get(`/api/users/${user.pubkey}`); + + if (!userResponse.data) { + showToast('error', 'Error', 'User not found', 'Please try again.'); + return; + } + const payload = { title, summary, - isPaidResource: checked, - price: checked ? price : null, + type: 'resource', + price: isPaidResource ? price : null, content: text, + image: coverImage, + user: userResponse.data.id, topics: topics.map(topic => topic.trim().toLowerCase()) }; - if (checked) { - broadcastPaidResource(payload); - } else { - broadcastFreeResource(payload); + if (payload && payload.user) { + axios.post('/api/drafts', payload) + .then(response => { + if (response.status === 201) { + showToast('success', 'Success', 'Resource saved as draft.'); + + if (response.data?.id) { + router.push(`/draft/${response.data.id}`); + } + } + }) + .catch(error => { + console.error(error); + }); } }; - const broadcastFreeResource = 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 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 signedEvent = await window.nostr.signEvent(event); - const eventVerification = await verifyEvent(signedEvent); + // const eventVerification = await verifyEvent(signedEvent); - if (!eventVerification) { - showToast('error', 'Error', 'Event verification failed. Please try again.'); - return; - } + // if (!eventVerification) { + // showToast('error', 'Error', 'Event verification failed. Please try again.'); + // return; + // } - const nAddress = nip19.naddrEncode({ - pubkey: signedEvent.pubkey, - kind: signedEvent.kind, - identifier: newresourceId, - }) + // const nAddress = nip19.naddrEncode({ + // pubkey: signedEvent.pubkey, + // kind: signedEvent.kind, + // identifier: newresourceId, + // }) - console.log('nAddress:', nAddress); + // console.log('nAddress:', nAddress); - const userResponse = await axios.get(`/api/users/${user.pubkey}`) + // const userResponse = await axios.get(`/api/users/${user.pubkey}`) - if (!userResponse.data) { - showToast('error', 'Error', 'User not found', 'Please try again.'); - return; - } + // 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); + // const resourcePayload = { + // id: newresourceId, + // userId: userResponse.data.id, + // price: 0, + // noteId: nAddress, + // } + // const response = await axios.post(`/api/resources`, resourcePayload); - console.log('response:', response); + // console.log('response:', response); - if (response.status !== 201) { - showToast('error', 'Error', 'Failed to create resource. Please try again.'); - return; - } + // if (response.status !== 201) { + // showToast('error', 'Error', 'Failed to create resource. Please try again.'); + // return; + // } - const publishResponse = await publishAll(signedEvent); + // 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}`); - }); - } + // 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}`); - }) - } + // 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 broadcastPaidResource = 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] - ] - }; + // // 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 signedEvent = await window.nostr.signEvent(event); - const eventVerification = await verifyEvent(signedEvent); + // const eventVerification = await verifyEvent(signedEvent); - if (!eventVerification) { - showToast('error', 'Error', 'Event verification failed. Please try again.'); - return; - } + // if (!eventVerification) { + // showToast('error', 'Error', 'Event verification failed. Please try again.'); + // return; + // } - const nAddress = nip19.naddrEncode({ - pubkey: signedEvent.pubkey, - kind: signedEvent.kind, - identifier: newresourceId, - }) + // const nAddress = nip19.naddrEncode({ + // pubkey: signedEvent.pubkey, + // kind: signedEvent.kind, + // identifier: newresourceId, + // }) - console.log('nAddress:', nAddress); + // 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 userResponse = await axios.get(`/api/users/${user.pubkey}`) - 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; - } + // if (!userResponse.data) { + // showToast('error', 'Error', 'User not found', 'Please try again.'); + // return; + // } - const publishResponse = await publishAll(signedEvent); + // const resourcePayload = { + // id: newresourceId, + // userId: userResponse.data.id, + // price: payload.price || 0, + // noteId: nAddress, + // } + // const response = await axios.post(`/api/resources`, resourcePayload); - 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}`); - }); - } + // if (response.status !== 201) { + // showToast('error', 'Error', 'Failed to create resource. Please try again.'); + // return; + // } - publishResponse?.successfulRelays.map(relay => { - showToast('success', 'Success', `Published to relay: ${relay}`); - }) - } + // 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); @@ -252,8 +275,8 @@ const ResourceForm = () => {

Paid Resource

- setChecked(e.value)} /> - {checked && ( + setIsPaidResource(e.value)} /> + {isPaidResource && (
setPrice(e.value)} placeholder="Price (sats)" />
diff --git a/src/db/models/courseModels.js b/src/db/models/courseModels.js index 31d95f8..f2025d6 100644 --- a/src/db/models/courseModels.js +++ b/src/db/models/courseModels.js @@ -1,9 +1,7 @@ import prisma from "../prisma"; -const client = new prisma.PrismaClient(); - export const getAllCourses = async () => { - return await client.course.findMany({ + return await prisma.course.findMany({ include: { resources: true, // Include related resources purchases: true, // Include related purchases @@ -12,7 +10,7 @@ export const getAllCourses = async () => { }; export const getCourseById = async (id) => { - return await client.course.findUnique({ + return await prisma.course.findUnique({ where: { id }, include: { resources: true, // Include related resources @@ -22,20 +20,20 @@ export const getCourseById = async (id) => { }; export const createCourse = async (data) => { - return await client.course.create({ + return await prisma.course.create({ data, }); }; export const updateCourse = async (id, data) => { - return await client.course.update({ + return await prisma.course.update({ where: { id }, data, }); }; export const deleteCourse = async (id) => { - return await client.course.delete({ + return await prisma.course.delete({ where: { id }, }); }; diff --git a/src/db/models/draftModels.js b/src/db/models/draftModels.js new file mode 100644 index 0000000..ae2ad61 --- /dev/null +++ b/src/db/models/draftModels.js @@ -0,0 +1,46 @@ +import prisma from "../prisma"; + +export const getAllDraftsByUserId = async (userId) => { + return await prisma.draft.findMany({ + where: { userId }, + include: { + user: true, + }, + }); +} + +export const getDraftById = async (id) => { + return await prisma.draft.findUnique({ + where: { id }, + include: { + user: true, + }, + }); +}; + +export const createDraft = async (data) => { + return await prisma.draft.create({ + data: { + ...data, + user: { + connect: { + id: data.user, + }, + }, + }, + }); +}; + + +export const updateDraft = async (id, data) => { + return await prisma.draft.update({ + where: { id }, + data, + }); +}; + +export const deleteDraft = async (id) => { + return await prisma.draft.delete({ + where: { id }, + }); +} diff --git a/src/db/prisma.js b/src/db/prisma.js index b904402..4e5830c 100644 --- a/src/db/prisma.js +++ b/src/db/prisma.js @@ -1,5 +1,28 @@ +// db/prisma.js + +// Import the PrismaClient class from the @prisma/client package. import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); +// Declare a variable to hold our Prisma client instance. +let prisma; -export default prisma; \ No newline at end of file +// Check if the application is running in a production environment. +if (process.env.NODE_ENV === 'production') { + // In production, always create a new instance of PrismaClient. + prisma = new PrismaClient(); +} else { + // In development or other non-production environments... + + // Check if there is already an instance of PrismaClient attached to the global object. + if (!global.prisma) { + // If not, create a new instance of PrismaClient... + global.prisma = new PrismaClient(); + } + // ...and assign it to the prisma variable. This ensures that in development, + // the same instance of PrismaClient is reused across hot reloads and server restarts, + // which can help prevent the exhaustion of database connections during development. + prisma = global.prisma; +} + +// Export the prisma client instance, making it available for import in other parts of the application. +export default prisma; diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index 7444e89..ce84462 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { SimplePool, nip19, verifyEvent } from "nostr-tools"; -import { Relay } from 'nostr-tools/relay' +import { useToast } from "./useToast"; const initialRelays = [ "wss://nos.lol/", @@ -22,6 +22,8 @@ export const useNostr = () => { streams: [] }); + const {showToast} = useToast(); + const pool = useRef(new SimplePool({ seenOnEnabled: true })); const subscriptions = useRef([]); @@ -200,8 +202,10 @@ export const useNostr = () => { results.forEach((result, i) => { if (result.status === 'fulfilled') { successfulRelays.push(relays[i]) + showToast('success', `published to ${relays[i]}`) } else { failedRelays.push(relays[i]) + showToast('error', `failed to publish to ${relays[i]}`) } }) diff --git a/src/hooks/useResponsiveImageDimensions.js b/src/hooks/useResponsiveImageDimensions.js new file mode 100644 index 0000000..ed78fa3 --- /dev/null +++ b/src/hooks/useResponsiveImageDimensions.js @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react'; + +const useResponsiveImageDimensions = () => { + // Initialize screenWidth with a default value for SSR + // This can be a typical screen width or the smallest size you want to target + const [screenWidth, setScreenWidth] = useState(0); + + useEffect(() => { + // Set the initial width on the client side + setScreenWidth(window.innerWidth); + + const handleResize = () => { + setScreenWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + + // Cleanup the event listener on component unmount + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + const calculateImageDimensions = () => { + if (screenWidth >= 1200) { + return { width: 426, height: 240 }; // Large screens + } else if (screenWidth >= 768 && screenWidth < 1200) { + return { width: 344, height: 194 }; // Medium screens + } else if (screenWidth > 0) { // Check if screenWidth is set to avoid incorrect rendering during SSR + return { width: screenWidth - 120, height: (screenWidth - 120) * (9 / 16) }; // Small screens + } else { + return { width: 0, height: 0 }; // Default sizes or SSR fallback + } + }; + + return calculateImageDimensions(); +}; + +export default useResponsiveImageDimensions; diff --git a/src/pages/_document.js b/src/pages/_document.js index 771da81..303f256 100644 --- a/src/pages/_document.js +++ b/src/pages/_document.js @@ -6,7 +6,6 @@ class MyDocument extends Document { - diff --git a/src/pages/api/drafts/[slug].js b/src/pages/api/drafts/[slug].js new file mode 100644 index 0000000..6963409 --- /dev/null +++ b/src/pages/api/drafts/[slug].js @@ -0,0 +1,40 @@ +import { getDraftById, updateDraft, deleteDraft } from "@/db/models/draftModels"; + +export default async function handler(req, res) { + const { slug } = req.query; + + if (req.method === 'GET') { + try { + const draft = await getDraftById(slug); + if (draft) { + res.status(200).json(draft); + } else { + res.status(404).json({ error: 'Draft not found' }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } + } else if (req.method === 'PUT') { + try { + const draft = await updateDraft(slug, req.body); + if (draft) { + res.status(200).json(draft); + } else { + res.status(400).json({ error: 'Draft not updated' }); + } + } catch (error) { + res.status(400).json({ error: error.message }); + } + } else if (req.method === 'DELETE') { + try { + await deleteDraft(slug); + res.status(204).end(); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } else { + // Handle any other HTTP method + res.setHeader('Allow', ['GET', 'PUT', 'DELETE']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/src/pages/api/drafts/all/[slug].js b/src/pages/api/drafts/all/[slug].js new file mode 100644 index 0000000..83d174e --- /dev/null +++ b/src/pages/api/drafts/all/[slug].js @@ -0,0 +1,22 @@ +import { getAllDraftsByUserId } from "@/db/models/draftModels"; + +export default async function handler(req, res) { +const { id } = req.query; + + if (req.method === 'GET') { + try { + const resource = await getAllDraftsByUserId(parseInt(id)); + if (resource) { + res.status(200).json(resource); + } else { + res.status(404).json({ error: 'Resource not found' }); + } + } catch (error) { + res.status(400).json({ error: error.message }); + } + } else { + // Handle any other HTTP method + res.setHeader('Allow', ['GET', 'POST']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/src/pages/api/drafts/index.js b/src/pages/api/drafts/index.js new file mode 100644 index 0000000..60d2c8b --- /dev/null +++ b/src/pages/api/drafts/index.js @@ -0,0 +1,20 @@ +import { createDraft } from "@/db/models/draftModels"; + +export default async function handler(req, res) { + if (req.method === 'POST') { + try { + const draft = await createDraft(req.body); + if (draft) { + res.status(201).json(draft); + } else { + res.status(400).json({ error: 'Draft not created' }); + } + } catch (error) { + res.status(400).json({ error: error.message }); + } + } else { + // Handle any other HTTP method + res.setHeader('Allow', ['GET', 'POST']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/src/pages/draft/[slug].js b/src/pages/draft/[slug].js new file mode 100644 index 0000000..961f1e7 --- /dev/null +++ b/src/pages/draft/[slug].js @@ -0,0 +1,239 @@ +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 } 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 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 }) => { + return ( +
+ + {content} + +
+ ); +}; + +export default function Details() { + const [draft, setDraft] = useState(null); + + const { returnImageProxy } = useImageProxy(); + const { fetchSingleEvent, fetchKind0 } = useNostr(); + + const [user] = useLocalStorageWithEffect('user', {}); + + const { width, height } = useResponsiveImageDimensions(); + + const router = useRouter(); + + const {showToast} = useToast(); + + const { publishAll } = useNostr(); + + 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) { + const {unsignedEvent, type} = buildEvent(draft); + + 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; + } + + await publishAll(signedEvent); + } + + const buildEvent = (draft) => { + const NewDTag = uuidv4(); + let event = {}; + let type; + + switch (draft?.type) { + case 'resource': + event = { + kind: draft?.price ? 30402 : 30023, // Determine kind based on if price is present + content: draft.content, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['d', NewDTag], + ['title', draft.title], + ['summary', draft.summary], + ['image', draft.image], + ['t', ...draft.topics], + ['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': + 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], + ['t', ...draft.topics], + ['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], + ['t', ...draft.topics], + ['published_at', Math.floor(Date.now() / 1000).toString()], + ] + }; + type = 'course'; + break; + default: + event = null; + type = 'unknown'; + } + + return { unsignedEvent: event, type }; + }; + + return ( +
+
+ {/* router.push('/')} /> */} +
+
+
+ + + + + +
+

{draft?.title}

+

{draft?.summary}

+
+ resource thumbnail +

+ Created by{' '} + + {user?.username} + +

+
+
+
+ {draft && ( +
router.push(`/details/${draft.id}`)} className="flex flex-col items-center mx-auto cursor-pointer rounded-md shadow-lg"> +
+ resource thumbnail +
+
+ )} +
+
+
+
+
+
+ { + draft?.content && + } +
+
+ ); +} \ No newline at end of file