Starting on drafts flow, updating forms, updating db, some small ui fixes

This commit is contained in:
austinkelsay 2024-03-25 13:39:32 -05:00
parent 187573088b
commit aca8c6ee82
14 changed files with 633 additions and 143 deletions

View File

@ -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;

View File

@ -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
}

View File

@ -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();

View File

@ -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>

View File

@ -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 },
});
};

View 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 },
});
}

View File

@ -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;

View File

@ -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]}`)
}
})

View 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;

View File

@ -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>

View 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`);
}
}

View 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`);
}
}

View 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
View 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>
);
}