mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +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,
|
||||
"price" INTEGER DEFAULT 0,
|
||||
"topics" TEXT[],
|
||||
"additionalLinks" TEXT[],
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
@ -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 {
|
||||
|
@ -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 && (
|
||||
<div className='flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md'>
|
||||
<Image
|
||||
alt="resource thumbnail"
|
||||
alt="course thumbnail"
|
||||
src={returnImageProxy(processedEvent.image)}
|
||||
width={344}
|
||||
height={194}
|
||||
|
@ -1,9 +1,19 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Tag } from "primereact/tag";
|
||||
import { Message } from "primereact/message";
|
||||
import { Button } from "primereact/button";
|
||||
import Image from "next/image";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
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";
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
@ -14,8 +24,21 @@ const MDDisplay = dynamic(
|
||||
);
|
||||
|
||||
const DraftCourseLesson = ({ lesson, course }) => {
|
||||
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 }) => {
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{lesson?.title}</h1>
|
||||
<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'>
|
||||
<Image
|
||||
alt="avatar thumbnail"
|
||||
@ -63,9 +100,17 @@ const DraftCourseLesson = ({ lesson, course }) => {
|
||||
}
|
||||
<div className='flex flex-row w-full mt-6 items-center'>
|
||||
{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>
|
||||
|
@ -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 (
|
||||
<form onSubmit={isPublished && draft ? handlePublishedResource : handleSubmit}>
|
||||
<div className="p-inputgroup flex-1">
|
||||
@ -224,6 +246,30 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
||||
/>
|
||||
</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">
|
||||
{topics.map((topic, index) => (
|
||||
<div className="p-inputgroup flex-1" key={index}>
|
||||
@ -244,4 +290,4 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceForm;
|
||||
export default ResourceForm;
|
@ -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 }) => {
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
|
||||
</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">
|
||||
{topics.map((topic, index) => (
|
||||
<div className="p-inputgroup flex-1" key={index}>
|
||||
@ -163,4 +203,4 @@ const WorkshopForm = ({ draft = null }) => {
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkshopForm;
|
||||
export default WorkshopForm;
|
@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{draft?.title}</h1>
|
||||
<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'>
|
||||
<Image
|
||||
alt="resource thumbnail"
|
||||
src={returnImageProxy(draft?.author?.avatar, draft?.author?.pubkey)}
|
||||
src={returnImageProxy(draft?.user?.avatar, draft?.user?.pubkey)}
|
||||
width={50}
|
||||
height={50}
|
||||
className="rounded-full mr-4"
|
||||
@ -273,6 +273,7 @@ export default function Draft() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="pt-8 text-sm text-gray-400">{draft?.createdAt && formatDateTime(draft?.createdAt)}</p>
|
||||
</div>
|
||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
||||
{draft && (
|
||||
@ -306,4 +307,4 @@ export default function Draft() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user