diff --git a/package-lock.json b/package-lock.json index d88b6c6..a9b7e3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,8 @@ "react-redux": "^9.1.0", "react-typist": "^2.0.5", "redux": "^5.0.1", - "rehype-raw": "^7.0.0" + "rehype-raw": "^7.0.0", + "uuid": "^9.0.1" }, "devDependencies": { "@types/node": "20.11.21", @@ -4498,6 +4499,14 @@ } } }, + "node_modules/next-auth/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6483,9 +6492,13 @@ "dev": true }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index d4dde79..523ce21 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "react-redux": "^9.1.0", "react-typist": "^2.0.5", "redux": "^5.0.1", - "rehype-raw": "^7.0.0" + "rehype-raw": "^7.0.0", + "uuid": "^9.0.1" }, "devDependencies": { "@types/node": "20.11.21", diff --git a/prisma/migrations/20240320164115_init/migration.sql b/prisma/migrations/20240320231910_init/migration.sql similarity index 89% rename from prisma/migrations/20240320164115_init/migration.sql rename to prisma/migrations/20240320231910_init/migration.sql index c6e642e..1392cb2 100644 --- a/prisma/migrations/20240320164115_init/migration.sql +++ b/prisma/migrations/20240320231910_init/migration.sql @@ -1,10 +1,10 @@ -- CreateTable CREATE TABLE "User" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "pubkey" TEXT NOT NULL, "username" TEXT, "avatar" TEXT, - "roleId" INTEGER, + "roleId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -13,7 +13,7 @@ CREATE TABLE "User" ( -- CreateTable CREATE TABLE "Role" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "subscribed" BOOLEAN NOT NULL DEFAULT false, CONSTRAINT "Role_pkey" PRIMARY KEY ("id") @@ -21,10 +21,10 @@ CREATE TABLE "Role" ( -- CreateTable CREATE TABLE "Purchase" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "courseId" INTEGER, - "resourceId" INTEGER, + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "courseId" TEXT, + "resourceId" TEXT, "amountPaid" INTEGER NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -34,8 +34,8 @@ CREATE TABLE "Purchase" ( -- CreateTable CREATE TABLE "Course" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, "price" INTEGER NOT NULL DEFAULT 0, "noteId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -46,9 +46,9 @@ CREATE TABLE "Course" ( -- CreateTable CREATE TABLE "Resource" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "courseId" INTEGER, + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "courseId" TEXT, "price" INTEGER NOT NULL DEFAULT 0, "noteId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d1046d9..6334e16 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,7 +8,7 @@ generator client { } model User { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) pubkey String @unique username String? @unique avatar String? @@ -16,52 +16,51 @@ model User { courses Course[] // Relation field added for courses created by the user resources Resource[] // Relation field added for resources created by the user role Role? @relation(fields: [roleId], references: [id]) - roleId Int? + roleId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Role { - id Int @id @default(autoincrement()) - subscribed Boolean @default(false) - users User[] + id String @id @default(uuid()) + subscribed Boolean @default(false) + users User[] } model Purchase { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) user User @relation(fields: [userId], references: [id]) - userId Int + userId String course Course? @relation(fields: [courseId], references: [id]) - courseId Int? + courseId String? resource Resource? @relation(fields: [resourceId], references: [id]) - resourceId Int? + resourceId String? amountPaid Int // in satoshis createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } - model Course { - id Int @id @default(autoincrement()) - userId Int // Field added to link a course to a user - user User @relation(fields: [userId], references: [id]) // Relation setup for user - price Int @default(0) + id String @id // Client generates UUID + userId String + user User @relation(fields: [userId], references: [id]) + price Int @default(0) resources Resource[] purchases Purchase[] - noteId String? @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + noteId String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Resource { - id Int @id @default(autoincrement()) - userId Int // Field added to link a resource to a user - user User @relation(fields: [userId], references: [id]) // Relation setup for user - course Course? @relation(fields: [courseId], references: [id]) - courseId Int? - price Int @default(0) + id String @id // Client generates UUID + userId String + user User @relation(fields: [userId], references: [id]) + course Course? @relation(fields: [courseId], references: [id]) + courseId String? + price Int @default(0) purchases Purchase[] - noteId String? @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + noteId String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/src/components/forms/ResourceForm.js b/src/components/forms/ResourceForm.js index 1a69dc8..562d0e9 100644 --- a/src/components/forms/ResourceForm.js +++ b/src/components/forms/ResourceForm.js @@ -1,9 +1,14 @@ import React, { useState } from "react"; +import axios from "axios"; import { InputText } from "primereact/inputtext"; import { InputNumber } from "primereact/inputnumber"; import { InputSwitch } from "primereact/inputswitch"; import { Editor } from "primereact/editor"; import { Button } from "primereact/button"; +import { nip04, verifyEvent, nip19 } from "nostr-tools"; +import { useNostr } from "@/hooks/useNostr"; +import { v4 as uuidv4 } from 'uuid'; +import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; import 'primeicons/primeicons.css'; const ResourceForm = () => { @@ -12,6 +17,11 @@ const ResourceForm = () => { const [checked, setChecked] = useState(false); const [price, setPrice] = useState(0); const [text, setText] = useState(''); + const [topics, setTopics] = useState(['']); // Initialize with an empty string to show one input by default + + const [user] = useLocalStorageWithEffect('user', {}); + + const { publishAll } = useNostr(); const handleSubmit = (e) => { e.preventDefault(); // Prevents the default form submission mechanism @@ -20,11 +30,84 @@ const ResourceForm = () => { summary, isPaidResource: checked, price: checked ? price : null, - content: text + content: text, + topics: topics.map(topic => topic.trim().toLowerCase()) // Process topics as they are }; - console.log(payload); + buildPaidResource(payload); + }; + + // 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 buildPaidResource = 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] + ] + }; + + console.log('event:', event); + + // Will need to add d tag from db id + // will need to add url/id as location tag + + // first sign the event + const signedEvent = await window.nostr.signEvent(event); + + const eventVerification = await verifyEvent(signedEvent); + + console.log('eventVerification:', eventVerification); + + console.log('signedEvent:', signedEvent); + + 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}`) + console.log('userResponse:', userResponse); + const resourcePayload = { + id: newresourceId, + userId: userResponse.data.id, + price: payload.price || 0, + noteId: nAddress, + } + const response = await axios.post(`/api/resources`, resourcePayload); + console.log('response:', response); + + const publishResponse = await publishAll(signedEvent); + console.log('publishResponse:', publishResponse); } + const handleTopicChange = (index, value) => { + const updatedTopics = topics.map((topic, i) => i === index ? value : topic); + setTopics(updatedTopics); + }; + + const addTopic = () => { + setTopics([...topics, '']); // Add an empty string to the topics array + }; + + const removeTopic = (index) => { + const updatedTopics = topics.filter((_, i) => i !== index); + setTopics(updatedTopics); + }; + return (