mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Starting on drafts flow, updating forms, updating db, some small ui fixes
This commit is contained in:
parent
187573088b
commit
aca8c6ee82
@ -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;
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 = () => {
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-8 flex-col">
|
||||
<p className="py-2">Paid Resource</p>
|
||||
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} />
|
||||
{checked && (
|
||||
<InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
|
||||
{isPaidResource && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
|
||||
</div>
|
||||
|
@ -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 },
|
||||
});
|
||||
};
|
||||
|
46
src/db/models/draftModels.js
Normal file
46
src/db/models/draftModels.js
Normal file
@ -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 },
|
||||
});
|
||||
}
|
@ -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;
|
||||
// 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;
|
||||
|
@ -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]}`)
|
||||
}
|
||||
})
|
||||
|
||||
|
39
src/hooks/useResponsiveImageDimensions.js
Normal file
39
src/hooks/useResponsiveImageDimensions.js
Normal file
@ -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;
|
@ -6,7 +6,6 @@ class MyDocument extends Document {
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Blinker:wght@100;200;300;400;600;700;800;900&family=Poppins&display=swap" rel="stylesheet" />
|
||||
</Head>
|
||||
<body>
|
||||
|
40
src/pages/api/drafts/[slug].js
Normal file
40
src/pages/api/drafts/[slug].js
Normal file
@ -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`);
|
||||
}
|
||||
}
|
22
src/pages/api/drafts/all/[slug].js
Normal file
22
src/pages/api/drafts/all/[slug].js
Normal file
@ -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`);
|
||||
}
|
||||
}
|
20
src/pages/api/drafts/index.js
Normal file
20
src/pages/api/drafts/index.js
Normal file
@ -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`);
|
||||
}
|
||||
}
|
239
src/pages/draft/[slug].js
Normal file
239
src/pages/draft/[slug].js
Normal file
@ -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 (
|
||||
<div>
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]} className='markdown-content'>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<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'>
|
||||
<Tag className='mr-2' value="Primary"></Tag>
|
||||
<Tag className='mr-2' severity="success" value="Success"></Tag>
|
||||
<Tag className='mr-2' severity="info" value="Info"></Tag>
|
||||
<Tag className='mr-2' severity="warning" value="Warning"></Tag>
|
||||
<Tag className='mr-2' severity="danger" value="Danger"></Tag>
|
||||
</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"
|
||||
/>
|
||||
<p className='text-lg'>
|
||||
Created by{' '}
|
||||
<a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
|
||||
{user?.username}
|
||||
</a>
|
||||
</p>
|
||||
</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'>
|
||||
<Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto my-2" />
|
||||
</div>
|
||||
<div className='w-[75vw] mx-auto mt-24 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]'>
|
||||
{
|
||||
draft?.content && <MarkdownContent content={draft.content} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user