From 347ca659d36ba4fcb8b641a03c0d66d4676ef43e Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Sat, 24 Aug 2024 17:10:00 -0500 Subject: [PATCH] Add additional links to drafts and published resources, updated forms and rendering accordingly --- .../migration.sql | 1 + prisma/schema.prisma | 32 +++++------ .../content/courses/DraftCourseDetails.js | 24 ++------- .../content/courses/DraftCourseLesson.js | 51 ++++++++++++++++-- src/components/forms/ResourceForm.js | 52 ++++++++++++++++-- src/components/forms/WorkshopForm.js | 54 ++++++++++++++++--- src/db/models/draftModels.js | 8 +-- src/pages/details/[slug]/edit.js | 2 +- src/pages/draft/[slug]/edit.js | 2 +- src/pages/draft/[slug]/index.js | 43 +++++++-------- src/utils/nostr.js | 26 +++++++++ 11 files changed, 220 insertions(+), 75 deletions(-) rename prisma/migrations/{20240823234011_init => 20240824210117_init}/migration.sql (99%) diff --git a/prisma/migrations/20240823234011_init/migration.sql b/prisma/migrations/20240824210117_init/migration.sql similarity index 99% rename from prisma/migrations/20240823234011_init/migration.sql rename to prisma/migrations/20240824210117_init/migration.sql index 370fea9..f228464 100644 --- a/prisma/migrations/20240823234011_init/migration.sql +++ b/prisma/migrations/20240824210117_init/migration.sql @@ -93,6 +93,7 @@ CREATE TABLE "Draft" ( "image" TEXT, "price" INTEGER DEFAULT 0, "topics" TEXT[], + "additionalLinks" TEXT[], "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 472a96a..d018aa4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -80,7 +80,6 @@ model Course { updatedAt DateTime @updatedAt } -// Additional resources model Resource { id String @id // Client generates UUID userId String @@ -94,22 +93,23 @@ model Resource { updatedAt DateTime @updatedAt } -// Additional resources + 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 - draftLessons DraftLesson[] - lessons Lesson[] + 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[] + additionalLinks String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + draftLessons DraftLesson[] + lessons Lesson[] } model CourseDraft { diff --git a/src/components/content/courses/DraftCourseDetails.js b/src/components/content/courses/DraftCourseDetails.js index 2573edc..2de8635 100644 --- a/src/components/content/courses/DraftCourseDetails.js +++ b/src/components/content/courses/DraftCourseDetails.js @@ -14,6 +14,7 @@ import { NDKEvent } from "@nostr-dev-kit/ndk"; import { findKind0Fields } from '@/utils/nostr'; import { useToast } from '@/hooks/useToast'; import { formatDateTime } from '@/utils/time'; +import { validateEvent } from '@/utils/nostr'; import 'primeicons/primeicons.css'; const MDDisplay = dynamic( @@ -23,25 +24,6 @@ const MDDisplay = dynamic( } ); -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 DraftCourseDetails({ processedEvent, draftId, lessons }) { const [author, setAuthor] = useState(null); const [user, setUser] = useState(null); @@ -259,6 +241,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), + ...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []), ]; type = 'resource'; @@ -281,6 +264,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), + ...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []), ]; type = 'workshop'; @@ -360,7 +344,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) {processedEvent && (
resource thumbnail { - const { returnImageProxy } = useImageProxy(); const [isPublished, setIsPublished] = useState(false); + const [user, setUser] = useState(null); + + const router = useRouter(); + const { returnImageProxy } = useImageProxy(); + const { ndk, addSigner } = useNDKContext(); + const { data: session } = useSession(); + const { showToast } = useToast(); + + useEffect(() => { + if (session) { + setUser(session.user); + } + }, [session]); + useEffect(() => { if (lesson?.kind) { console.log(lesson); @@ -39,6 +62,20 @@ const DraftCourseLesson = ({ lesson, course }) => {

{lesson?.title}

{lesson?.summary}

+ {lesson?.additionalLinks && lesson.additionalLinks.length > 0 && ( +
+

Additional links:

+ +
+ )}
avatar thumbnail { }
{isPublished ? ( - + <> + +
diff --git a/src/components/forms/ResourceForm.js b/src/components/forms/ResourceForm.js index 73aa42f..6e05444 100644 --- a/src/components/forms/ResourceForm.js +++ b/src/components/forms/ResourceForm.js @@ -17,6 +17,8 @@ const MDEditor = dynamic( } ); import 'primeicons/primeicons.css'; +import { Tooltip } from 'primereact/tooltip'; +import 'primereact/resources/primereact.min.css'; const ResourceForm = ({ draft = null, isPublished = false }) => { const [title, setTitle] = useState(draft?.title || ''); @@ -27,6 +29,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { const [topics, setTopics] = useState(draft?.topics || ['']); const [content, setContent] = useState(draft?.content || ''); const [user, setUser] = useState(null); + const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']); const { data: session, status } = useSession(); const { showToast } = useToast(); @@ -57,6 +60,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { setContent(draft.content); setCoverImage(draft.image); setTopics(draft.topics || []); + setAdditionalLinks(draft.additionalLinks || []); } }, [draft]); @@ -98,7 +102,8 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { content, d: draft.d, image: coverImage, - topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'resource'] + topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'resource'], + additionalLinks: additionalLinks.filter(link => link.trim() !== '') } console.log('handlePublishedResource', updatedDraft); @@ -148,7 +153,8 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { price: isPaidResource ? price : null, content, image: coverImage, - topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'resource'] + topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'resource'], + additionalLinks: additionalLinks.filter(link => link.trim() !== '') }; if (!draft) { @@ -193,6 +199,22 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { setTopics(updatedTopics); }; + const handleAdditionalLinkChange = (index, value) => { + const updatedAdditionalLinks = additionalLinks.map((link, i) => i === index ? value : link); + setAdditionalLinks(updatedAdditionalLinks); + }; + + const addAdditionalLink = (e) => { + e.preventDefault(); + setAdditionalLinks([...additionalLinks, '']); // Add an empty string to the additionalLinks array + }; + + const removeAdditionalLink = (e, index) => { + e.preventDefault(); + const updatedAdditionalLinks = additionalLinks.filter((_, i) => i !== index); + setAdditionalLinks(updatedAdditionalLinks); + }; + return (
@@ -224,6 +246,30 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { />
+
+ + Additional Links + + + {additionalLinks.map((link, index) => ( +
+ handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" /> + {index > 0 && ( +
+ ))} +
+
+ +
{topics.map((topic, index) => (
@@ -244,4 +290,4 @@ const ResourceForm = ({ draft = null, isPublished = false }) => { ); } -export default ResourceForm; +export default ResourceForm; \ No newline at end of file diff --git a/src/components/forms/WorkshopForm.js b/src/components/forms/WorkshopForm.js index 29976c1..4a5f456 100644 --- a/src/components/forms/WorkshopForm.js +++ b/src/components/forms/WorkshopForm.js @@ -8,6 +8,8 @@ import { Button } from 'primereact/button'; import { useToast } from '@/hooks/useToast'; import { useSession } from 'next-auth/react'; import 'primeicons/primeicons.css'; +import { Tooltip } from 'primereact/tooltip'; +import 'primereact/resources/primereact.min.css'; const WorkshopForm = ({ draft = null }) => { const [title, setTitle] = useState(draft?.title || ''); @@ -17,6 +19,7 @@ const WorkshopForm = ({ draft = null }) => { const [videoUrl, setVideoUrl] = useState(draft?.content || ''); const [coverImage, setCoverImage] = useState(draft?.image || ''); const [topics, setTopics] = useState(draft?.topics || ['']); + const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']); const router = useRouter(); const { data: session, status } = useSession(); @@ -38,6 +41,7 @@ const WorkshopForm = ({ draft = null }) => { setVideoUrl(draft.content); setCoverImage(draft.image); setTopics(draft.topics || ['']); + setAdditionalLinks(draft.additionalLinks || ['']); } }, [draft]); @@ -72,7 +76,8 @@ const WorkshopForm = ({ draft = null }) => { content: embedCode, image: coverImage, user: userResponse.data.id, - topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'workshop'] + topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'workshop'], + additionalLinks: additionalLinks.filter(link => link.trim() !== ''), }; if (payload && payload.user) { @@ -96,11 +101,6 @@ const WorkshopForm = ({ draft = null }) => { } }; - const onUpload = (event) => { - showToast('success', 'Success', 'File Uploaded'); - console.log(event.files[0]); - } - const handleTopicChange = (index, value) => { const updatedTopics = topics.map((topic, i) => i === index ? value : topic); setTopics(updatedTopics); @@ -116,6 +116,22 @@ const WorkshopForm = ({ draft = null }) => { const updatedTopics = topics.filter((_, i) => i !== index); setTopics(updatedTopics); }; + + const handleLinkChange = (index, value) => { + const updatedLinks = additionalLinks.map((link, i) => i === index ? value : link); + setAdditionalLinks(updatedLinks); + }; + + const addLink = (e) => { + e.preventDefault(); + setAdditionalLinks([...additionalLinks, '']); + }; + + const removeLink = (e, index) => { + e.preventDefault(); + const updatedLinks = additionalLinks.filter((_, i) => i !== index); + setAdditionalLinks(updatedLinks); + }; return ( @@ -143,6 +159,30 @@ const WorkshopForm = ({ draft = null }) => {
setCoverImage(e.target.value)} placeholder="Cover Image URL" />
+
+ + Additional Links + + + {additionalLinks.map((link, index) => ( +
+ handleLinkChange(index, e.target.value)} placeholder="https://example.com" className="w-full mt-2" /> + {index > 0 && ( +
+ ))} +
+
+ +
{topics.map((topic, index) => (
@@ -163,4 +203,4 @@ const WorkshopForm = ({ draft = null }) => { ); } -export default WorkshopForm; +export default WorkshopForm; \ No newline at end of file diff --git a/src/db/models/draftModels.js b/src/db/models/draftModels.js index dc16d47..f00deb1 100644 --- a/src/db/models/draftModels.js +++ b/src/db/models/draftModels.js @@ -27,20 +27,22 @@ export const createDraft = async (data) => { id: data.user, }, }, + additionalLinks: data.additionalLinks || [], }, }); }; export const updateDraft = async (id, data) => { - const { user, ...otherData } = data; + const { user, additionalLinks, ...otherData } = data; return await prisma.draft.update({ where: { id }, data: { ...otherData, user: user ? { connect: { id: user } - } : undefined + } : undefined, + additionalLinks: additionalLinks || undefined, }, }); }; @@ -49,4 +51,4 @@ export const deleteDraft = async (id) => { return await prisma.draft.delete({ where: { id }, }); -} +} \ No newline at end of file diff --git a/src/pages/details/[slug]/edit.js b/src/pages/details/[slug]/edit.js index b5919ff..84a3b89 100644 --- a/src/pages/details/[slug]/edit.js +++ b/src/pages/details/[slug]/edit.js @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { parseEvent } from "@/utils/nostr"; import ResourceForm from "@/components/forms/ResourceForm"; import WorkshopForm from "@/components/forms/WorkshopForm"; -import CourseForm from "@/components/forms/CourseForm"; +import CourseForm from "@/components/forms/course/CourseForm"; import { useNDKContext } from "@/context/NDKContext"; import { useToast } from "@/hooks/useToast"; diff --git a/src/pages/draft/[slug]/edit.js b/src/pages/draft/[slug]/edit.js index 39a7925..f730028 100644 --- a/src/pages/draft/[slug]/edit.js +++ b/src/pages/draft/[slug]/edit.js @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import axios from "axios"; import ResourceForm from "@/components/forms/ResourceForm"; import WorkshopForm from "@/components/forms/WorkshopForm"; -import CourseForm from "@/components/forms/CourseForm"; +import CourseForm from "@/components/forms/course/CourseForm"; const Edit = () => { const [draft, setDraft] = useState(null); diff --git a/src/pages/draft/[slug]/index.js b/src/pages/draft/[slug]/index.js index 34996f9..bee1b5a 100644 --- a/src/pages/draft/[slug]/index.js +++ b/src/pages/draft/[slug]/index.js @@ -11,10 +11,13 @@ import { useToast } from '@/hooks/useToast'; import { Tag } from 'primereact/tag'; import { useNDKContext } from '@/context/NDKContext'; import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { formatDateTime } from '@/utils/time'; import Image from 'next/image'; import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions'; import 'primeicons/primeicons.css'; import dynamic from 'next/dynamic'; +import { validateEvent } from '@/utils/nostr'; + const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), { @@ -22,25 +25,6 @@ const MDDisplay = dynamic( } ); -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 { returnImageProxy } = useImageProxy(); @@ -206,6 +190,7 @@ export default function Draft() { ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), + ...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []), ]; type = 'resource'; @@ -228,6 +213,7 @@ export default function Draft() { ...draft.topics.map(topic => ['t', topic]), ['published_at', Math.floor(Date.now() / 1000).toString()], ...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []), + ...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []), ]; type = 'workshop'; @@ -256,10 +242,24 @@ export default function Draft() {

{draft?.title}

{draft?.summary}

+ {draft?.additionalLinks && draft.additionalLinks.length > 0 && ( +
+

Additional links:

+ +
+ )}
resource thumbnail )}
+

{draft?.createdAt && formatDateTime(draft?.createdAt)}

{draft && ( @@ -306,4 +307,4 @@ export default function Draft() {
); -} +} \ No newline at end of file diff --git a/src/utils/nostr.js b/src/utils/nostr.js index 61d4f53..690ac6b 100644 --- a/src/utils/nostr.js +++ b/src/utils/nostr.js @@ -36,6 +36,7 @@ export const parseEvent = (event) => { pubkey: event.pubkey || '', content: event.content || '', kind: event.kind || '', + additionalLinks: [], title: '', summary: '', image: '', @@ -83,6 +84,9 @@ export const parseEvent = (event) => { case 't': tag[1] !== "plebdevs" && eventData.topics.push(tag[1]); break; + case 'r': + eventData.additionalLinks.push(tag[1]); + break; default: break; } @@ -144,6 +148,9 @@ export const parseCourseEvent = (event) => { eventData.topics.push(topic); }); break; + case 'r': + eventData.additionalLinks.push(tag[1]); + break; default: break; } @@ -155,3 +162,22 @@ export const parseCourseEvent = (event) => { export const hexToNpub = (hex) => { return nip19.npubEncode(hex); } + +export 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; +}