mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-05-20 17:02:04 +00:00
Add additional links to drafts and published resources, updated forms and rendering accordingly
This commit is contained in:
parent
b2d9d2bbe6
commit
347ca659d3
@ -93,6 +93,7 @@ CREATE TABLE "Draft" (
|
|||||||
"image" TEXT,
|
"image" TEXT,
|
||||||
"price" INTEGER DEFAULT 0,
|
"price" INTEGER DEFAULT 0,
|
||||||
"topics" TEXT[],
|
"topics" TEXT[],
|
||||||
|
"additionalLinks" TEXT[],
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
@ -80,7 +80,6 @@ model Course {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional resources
|
|
||||||
model Resource {
|
model Resource {
|
||||||
id String @id // Client generates UUID
|
id String @id // Client generates UUID
|
||||||
userId String
|
userId String
|
||||||
@ -94,22 +93,23 @@ model Resource {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional resources
|
|
||||||
model Draft {
|
model Draft {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
type String
|
type String
|
||||||
title String
|
title String
|
||||||
summary String
|
summary String
|
||||||
content String
|
content String
|
||||||
image String?
|
image String?
|
||||||
price Int? @default(0)
|
price Int? @default(0)
|
||||||
topics String[]
|
topics String[]
|
||||||
createdAt DateTime @default(now())
|
additionalLinks String[]
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
draftLessons DraftLesson[]
|
updatedAt DateTime @updatedAt
|
||||||
lessons Lesson[]
|
draftLessons DraftLesson[]
|
||||||
|
lessons Lesson[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model CourseDraft {
|
model CourseDraft {
|
||||||
|
@ -14,6 +14,7 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|||||||
import { findKind0Fields } from '@/utils/nostr';
|
import { findKind0Fields } from '@/utils/nostr';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { formatDateTime } from '@/utils/time';
|
import { formatDateTime } from '@/utils/time';
|
||||||
|
import { validateEvent } from '@/utils/nostr';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
const MDDisplay = dynamic(
|
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 }) {
|
export default function DraftCourseDetails({ processedEvent, draftId, lessons }) {
|
||||||
const [author, setAuthor] = useState(null);
|
const [author, setAuthor] = useState(null);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
@ -259,6 +241,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
|
|||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
|
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
type = 'resource';
|
type = 'resource';
|
||||||
@ -281,6 +264,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
|
|||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
|
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
type = 'workshop';
|
type = 'workshop';
|
||||||
@ -360,7 +344,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
|
|||||||
{processedEvent && (
|
{processedEvent && (
|
||||||
<div className='flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md'>
|
<div className='flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md'>
|
||||||
<Image
|
<Image
|
||||||
alt="resource thumbnail"
|
alt="course thumbnail"
|
||||||
src={returnImageProxy(processedEvent.image)}
|
src={returnImageProxy(processedEvent.image)}
|
||||||
width={344}
|
width={344}
|
||||||
height={194}
|
height={194}
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Tag } from "primereact/tag";
|
import { Tag } from "primereact/tag";
|
||||||
import { Message } from "primereact/message";
|
import { Message } from "primereact/message";
|
||||||
|
import { Button } from "primereact/button";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { formatDateTime, formatUnixTimestamp } from "@/utils/time";
|
import { formatDateTime, formatUnixTimestamp } from "@/utils/time";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
|
import { nip04, nip19 } from "nostr-tools";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
import { validateEvent } from "@/utils/nostr";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const MDDisplay = dynamic(
|
const MDDisplay = dynamic(
|
||||||
@ -14,8 +24,21 @@ const MDDisplay = dynamic(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const DraftCourseLesson = ({ lesson, course }) => {
|
const DraftCourseLesson = ({ lesson, course }) => {
|
||||||
const { returnImageProxy } = useImageProxy();
|
|
||||||
const [isPublished, setIsPublished] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (lesson?.kind) {
|
if (lesson?.kind) {
|
||||||
console.log(lesson);
|
console.log(lesson);
|
||||||
@ -39,6 +62,20 @@ const DraftCourseLesson = ({ lesson, course }) => {
|
|||||||
</div>
|
</div>
|
||||||
<h1 className='text-4xl mt-6'>{lesson?.title}</h1>
|
<h1 className='text-4xl mt-6'>{lesson?.title}</h1>
|
||||||
<p className='text-xl mt-6'>{lesson?.summary}</p>
|
<p className='text-xl mt-6'>{lesson?.summary}</p>
|
||||||
|
{lesson?.additionalLinks && lesson.additionalLinks.length > 0 && (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<h3 className='text-lg font-semibold mb-2'>Additional links:</h3>
|
||||||
|
<ul className='list-disc list-inside'>
|
||||||
|
{lesson.additionalLinks.map((link, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<a href={link} target="_blank" rel="noopener noreferrer" className='text-blue-500 hover:underline'>
|
||||||
|
{new URL(link).hostname}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='flex flex-row w-full mt-6 items-center'>
|
<div className='flex flex-row w-full mt-6 items-center'>
|
||||||
<Image
|
<Image
|
||||||
alt="avatar thumbnail"
|
alt="avatar thumbnail"
|
||||||
@ -63,9 +100,17 @@ const DraftCourseLesson = ({ lesson, course }) => {
|
|||||||
}
|
}
|
||||||
<div className='flex flex-row w-full mt-6 items-center'>
|
<div className='flex flex-row w-full mt-6 items-center'>
|
||||||
{isPublished ? (
|
{isPublished ? (
|
||||||
<Message severity="success" text="published" />
|
<>
|
||||||
|
<Message severity="success" text="published" />
|
||||||
|
<Button onClick={() => router.push(`/details/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
|
||||||
|
<Button onClick={() => router.push(`/details/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Message severity="info" text="draft" />
|
<>
|
||||||
|
<Message severity="info" text="draft (unpublished)" />
|
||||||
|
<Button onClick={() => router.push(`/draft/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
|
||||||
|
<Button onClick={() => router.push(`/draft/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,8 @@ const MDEditor = dynamic(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
|
import { Tooltip } from 'primereact/tooltip';
|
||||||
|
import 'primereact/resources/primereact.min.css';
|
||||||
|
|
||||||
const ResourceForm = ({ draft = null, isPublished = false }) => {
|
const ResourceForm = ({ draft = null, isPublished = false }) => {
|
||||||
const [title, setTitle] = useState(draft?.title || '');
|
const [title, setTitle] = useState(draft?.title || '');
|
||||||
@ -27,6 +29,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
|||||||
const [topics, setTopics] = useState(draft?.topics || ['']);
|
const [topics, setTopics] = useState(draft?.topics || ['']);
|
||||||
const [content, setContent] = useState(draft?.content || '');
|
const [content, setContent] = useState(draft?.content || '');
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
|
const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']);
|
||||||
|
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
@ -57,6 +60,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
|||||||
setContent(draft.content);
|
setContent(draft.content);
|
||||||
setCoverImage(draft.image);
|
setCoverImage(draft.image);
|
||||||
setTopics(draft.topics || []);
|
setTopics(draft.topics || []);
|
||||||
|
setAdditionalLinks(draft.additionalLinks || []);
|
||||||
}
|
}
|
||||||
}, [draft]);
|
}, [draft]);
|
||||||
|
|
||||||
@ -98,7 +102,8 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
|||||||
content,
|
content,
|
||||||
d: draft.d,
|
d: draft.d,
|
||||||
image: coverImage,
|
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);
|
console.log('handlePublishedResource', updatedDraft);
|
||||||
@ -148,7 +153,8 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
|||||||
price: isPaidResource ? price : null,
|
price: isPaidResource ? price : null,
|
||||||
content,
|
content,
|
||||||
image: coverImage,
|
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) {
|
if (!draft) {
|
||||||
@ -193,6 +199,22 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
|||||||
setTopics(updatedTopics);
|
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 (
|
return (
|
||||||
<form onSubmit={isPublished && draft ? handlePublishedResource : handleSubmit}>
|
<form onSubmit={isPublished && draft ? handlePublishedResource : handleSubmit}>
|
||||||
<div className="p-inputgroup flex-1">
|
<div className="p-inputgroup flex-1">
|
||||||
@ -224,6 +246,30 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-8 flex-col w-full">
|
||||||
|
<span className="pl-1 flex items-center">
|
||||||
|
Additional Links
|
||||||
|
<i className="pi pi-info-circle ml-2 cursor-pointer"
|
||||||
|
data-pr-tooltip="Add any relevant links that pair with this content"
|
||||||
|
data-pr-position="right"
|
||||||
|
data-pr-at="right+5 top"
|
||||||
|
data-pr-my="left center-2"
|
||||||
|
style={{ fontSize: '1rem', color: 'var(--primary-color)' }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{additionalLinks.map((link, index) => (
|
||||||
|
<div className="p-inputgroup flex-1" key={index}>
|
||||||
|
<InputText value={link} onChange={(e) => handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" />
|
||||||
|
{index > 0 && (
|
||||||
|
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="w-full flex flex-row items-end justify-end py-2">
|
||||||
|
<Button icon="pi pi-plus" onClick={addAdditionalLink} />
|
||||||
|
</div>
|
||||||
|
<Tooltip target=".pi-info-circle" />
|
||||||
|
</div>
|
||||||
<div className="mt-8 flex-col w-full">
|
<div className="mt-8 flex-col w-full">
|
||||||
{topics.map((topic, index) => (
|
{topics.map((topic, index) => (
|
||||||
<div className="p-inputgroup flex-1" key={index}>
|
<div className="p-inputgroup flex-1" key={index}>
|
||||||
|
@ -8,6 +8,8 @@ import { Button } from 'primereact/button';
|
|||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
|
import { Tooltip } from 'primereact/tooltip';
|
||||||
|
import 'primereact/resources/primereact.min.css';
|
||||||
|
|
||||||
const WorkshopForm = ({ draft = null }) => {
|
const WorkshopForm = ({ draft = null }) => {
|
||||||
const [title, setTitle] = useState(draft?.title || '');
|
const [title, setTitle] = useState(draft?.title || '');
|
||||||
@ -17,6 +19,7 @@ const WorkshopForm = ({ draft = null }) => {
|
|||||||
const [videoUrl, setVideoUrl] = useState(draft?.content || '');
|
const [videoUrl, setVideoUrl] = useState(draft?.content || '');
|
||||||
const [coverImage, setCoverImage] = useState(draft?.image || '');
|
const [coverImage, setCoverImage] = useState(draft?.image || '');
|
||||||
const [topics, setTopics] = useState(draft?.topics || ['']);
|
const [topics, setTopics] = useState(draft?.topics || ['']);
|
||||||
|
const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
@ -38,6 +41,7 @@ const WorkshopForm = ({ draft = null }) => {
|
|||||||
setVideoUrl(draft.content);
|
setVideoUrl(draft.content);
|
||||||
setCoverImage(draft.image);
|
setCoverImage(draft.image);
|
||||||
setTopics(draft.topics || ['']);
|
setTopics(draft.topics || ['']);
|
||||||
|
setAdditionalLinks(draft.additionalLinks || ['']);
|
||||||
}
|
}
|
||||||
}, [draft]);
|
}, [draft]);
|
||||||
|
|
||||||
@ -72,7 +76,8 @@ const WorkshopForm = ({ draft = null }) => {
|
|||||||
content: embedCode,
|
content: embedCode,
|
||||||
image: coverImage,
|
image: coverImage,
|
||||||
user: userResponse.data.id,
|
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) {
|
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 handleTopicChange = (index, value) => {
|
||||||
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
||||||
setTopics(updatedTopics);
|
setTopics(updatedTopics);
|
||||||
@ -117,6 +117,22 @@ const WorkshopForm = ({ draft = null }) => {
|
|||||||
setTopics(updatedTopics);
|
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 (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@ -143,6 +159,30 @@ const WorkshopForm = ({ draft = null }) => {
|
|||||||
<div className="p-inputgroup flex-1 mt-4">
|
<div className="p-inputgroup flex-1 mt-4">
|
||||||
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
|
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-8 flex-col w-full">
|
||||||
|
<span className="pl-1 flex items-center">
|
||||||
|
Additional Links
|
||||||
|
<i className="pi pi-info-circle ml-2 cursor-pointer"
|
||||||
|
data-pr-tooltip="Add any relevant or additional links that pair with this content"
|
||||||
|
data-pr-position="right"
|
||||||
|
data-pr-at="right+5 top"
|
||||||
|
data-pr-my="left center-2"
|
||||||
|
style={{ fontSize: '1rem', color: 'var(--primary-color)' }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{additionalLinks.map((link, index) => (
|
||||||
|
<div className="p-inputgroup flex-1" key={index}>
|
||||||
|
<InputText value={link} onChange={(e) => handleLinkChange(index, e.target.value)} placeholder="https://example.com" className="w-full mt-2" />
|
||||||
|
{index > 0 && (
|
||||||
|
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeLink(e, index)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="w-full flex flex-row items-end justify-end py-2">
|
||||||
|
<Button icon="pi pi-plus" onClick={addLink} />
|
||||||
|
</div>
|
||||||
|
<Tooltip target=".pi-info-circle" />
|
||||||
|
</div>
|
||||||
<div className="mt-4 flex-col w-full">
|
<div className="mt-4 flex-col w-full">
|
||||||
{topics.map((topic, index) => (
|
{topics.map((topic, index) => (
|
||||||
<div className="p-inputgroup flex-1" key={index}>
|
<div className="p-inputgroup flex-1" key={index}>
|
||||||
|
@ -27,20 +27,22 @@ export const createDraft = async (data) => {
|
|||||||
id: data.user,
|
id: data.user,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
additionalLinks: data.additionalLinks || [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const updateDraft = async (id, data) => {
|
export const updateDraft = async (id, data) => {
|
||||||
const { user, ...otherData } = data;
|
const { user, additionalLinks, ...otherData } = data;
|
||||||
return await prisma.draft.update({
|
return await prisma.draft.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
...otherData,
|
...otherData,
|
||||||
user: user ? {
|
user: user ? {
|
||||||
connect: { id: user }
|
connect: { id: user }
|
||||||
} : undefined
|
} : undefined,
|
||||||
|
additionalLinks: additionalLinks || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { parseEvent } from "@/utils/nostr";
|
import { parseEvent } from "@/utils/nostr";
|
||||||
import ResourceForm from "@/components/forms/ResourceForm";
|
import ResourceForm from "@/components/forms/ResourceForm";
|
||||||
import WorkshopForm from "@/components/forms/WorkshopForm";
|
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 { useNDKContext } from "@/context/NDKContext";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ResourceForm from "@/components/forms/ResourceForm";
|
import ResourceForm from "@/components/forms/ResourceForm";
|
||||||
import WorkshopForm from "@/components/forms/WorkshopForm";
|
import WorkshopForm from "@/components/forms/WorkshopForm";
|
||||||
import CourseForm from "@/components/forms/CourseForm";
|
import CourseForm from "@/components/forms/course/CourseForm";
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = () => {
|
||||||
const [draft, setDraft] = useState(null);
|
const [draft, setDraft] = useState(null);
|
||||||
|
@ -11,10 +11,13 @@ import { useToast } from '@/hooks/useToast';
|
|||||||
import { Tag } from 'primereact/tag';
|
import { Tag } from 'primereact/tag';
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import { formatDateTime } from '@/utils/time';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions';
|
import useResponsiveImageDimensions from '@/hooks/useResponsiveImageDimensions';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
import { validateEvent } from '@/utils/nostr';
|
||||||
|
|
||||||
const MDDisplay = dynamic(
|
const MDDisplay = dynamic(
|
||||||
() => import("@uiw/react-markdown-preview"),
|
() => 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() {
|
export default function Draft() {
|
||||||
const [draft, setDraft] = useState(null);
|
const [draft, setDraft] = useState(null);
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
@ -206,6 +190,7 @@ export default function Draft() {
|
|||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
|
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
type = 'resource';
|
type = 'resource';
|
||||||
@ -228,6 +213,7 @@ export default function Draft() {
|
|||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
|
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
type = 'workshop';
|
type = 'workshop';
|
||||||
@ -256,10 +242,24 @@ export default function Draft() {
|
|||||||
</div>
|
</div>
|
||||||
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
||||||
<p className='text-xl mt-6'>{draft?.summary}</p>
|
<p className='text-xl mt-6'>{draft?.summary}</p>
|
||||||
|
{draft?.additionalLinks && draft.additionalLinks.length > 0 && (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<h3 className='text-lg font-semibold mb-2'>Additional links:</h3>
|
||||||
|
<ul className='list-disc list-inside'>
|
||||||
|
{draft.additionalLinks.map((link, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<a href={link} target="_blank" rel="noopener noreferrer" className='text-blue-500 hover:underline'>
|
||||||
|
{new URL(link).hostname}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='flex flex-row w-full mt-6 items-center'>
|
<div className='flex flex-row w-full mt-6 items-center'>
|
||||||
<Image
|
<Image
|
||||||
alt="resource thumbnail"
|
alt="resource thumbnail"
|
||||||
src={returnImageProxy(draft?.author?.avatar, draft?.author?.pubkey)}
|
src={returnImageProxy(draft?.user?.avatar, draft?.user?.pubkey)}
|
||||||
width={50}
|
width={50}
|
||||||
height={50}
|
height={50}
|
||||||
className="rounded-full mr-4"
|
className="rounded-full mr-4"
|
||||||
@ -273,6 +273,7 @@ export default function Draft() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="pt-8 text-sm text-gray-400">{draft?.createdAt && formatDateTime(draft?.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
||||||
{draft && (
|
{draft && (
|
||||||
|
@ -36,6 +36,7 @@ export const parseEvent = (event) => {
|
|||||||
pubkey: event.pubkey || '',
|
pubkey: event.pubkey || '',
|
||||||
content: event.content || '',
|
content: event.content || '',
|
||||||
kind: event.kind || '',
|
kind: event.kind || '',
|
||||||
|
additionalLinks: [],
|
||||||
title: '',
|
title: '',
|
||||||
summary: '',
|
summary: '',
|
||||||
image: '',
|
image: '',
|
||||||
@ -83,6 +84,9 @@ export const parseEvent = (event) => {
|
|||||||
case 't':
|
case 't':
|
||||||
tag[1] !== "plebdevs" && eventData.topics.push(tag[1]);
|
tag[1] !== "plebdevs" && eventData.topics.push(tag[1]);
|
||||||
break;
|
break;
|
||||||
|
case 'r':
|
||||||
|
eventData.additionalLinks.push(tag[1]);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -144,6 +148,9 @@ export const parseCourseEvent = (event) => {
|
|||||||
eventData.topics.push(topic);
|
eventData.topics.push(topic);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'r':
|
||||||
|
eventData.additionalLinks.push(tag[1]);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -155,3 +162,22 @@ export const parseCourseEvent = (event) => {
|
|||||||
export const hexToNpub = (hex) => {
|
export const hexToNpub = (hex) => {
|
||||||
return nip19.npubEncode(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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user