mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
A lotta good stuff
This commit is contained in:
parent
80edbf0905
commit
b953b76785
@ -74,6 +74,26 @@ CREATE TABLE "Draft" (
|
||||
CONSTRAINT "Draft_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CourseDraft" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"image" TEXT,
|
||||
"price" INTEGER DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CourseDraft_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_CourseDraftToResource" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
|
||||
|
||||
@ -86,6 +106,12 @@ CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId");
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_CourseDraftToResource_AB_unique" ON "_CourseDraftToResource"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_CourseDraftToResource_B_index" ON "_CourseDraftToResource"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@ -109,3 +135,12 @@ ALTER TABLE "Resource" ADD CONSTRAINT "Resource_courseId_fkey" FOREIGN KEY ("cou
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Draft" ADD CONSTRAINT "Draft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CourseDraft" ADD CONSTRAINT "CourseDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_CourseDraftToResource" ADD CONSTRAINT "_CourseDraftToResource_A_fkey" FOREIGN KEY ("A") REFERENCES "CourseDraft"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_CourseDraftToResource" ADD CONSTRAINT "_CourseDraftToResource_B_fkey" FOREIGN KEY ("B") REFERENCES "Resource"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -16,6 +16,7 @@ model User {
|
||||
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
|
||||
courseDrafts CourseDraft[] // Relation field added for course drafts created by the user
|
||||
role Role? @relation(fields: [roleId], references: [id])
|
||||
roleId String?
|
||||
createdAt DateTime @default(now())
|
||||
@ -61,6 +62,7 @@ model Resource {
|
||||
courseId String?
|
||||
price Int @default(0)
|
||||
purchases Purchase[]
|
||||
courseDrafts CourseDraft[]
|
||||
noteId String? @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@ -81,3 +83,15 @@ model Draft {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CourseDraft {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
resources Resource[]
|
||||
title String
|
||||
summary String
|
||||
image String?
|
||||
price Int? @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, {useEffect} from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "primereact/button";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
@ -8,6 +8,25 @@ const ContentListItem = (content) => {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const router = useRouter();
|
||||
const isDraft = Object.keys(content).includes('type');
|
||||
const isCourse = content && content?.resources && content?.resources?.length > 0;
|
||||
|
||||
const handleClick = () => {
|
||||
let path = '';
|
||||
|
||||
if (isDraft) {
|
||||
path = '/draft';
|
||||
} else if (isCourse) {
|
||||
path = '/course';
|
||||
} else {
|
||||
path = '/details';
|
||||
}
|
||||
|
||||
const draftSuffix = isCourse ? '/draft' : '';
|
||||
const fullPath = `${path}/${content.id}${draftSuffix}`;
|
||||
|
||||
router.push(fullPath);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-4 border-bottom-1 surface-border" key={content.id}>
|
||||
@ -26,7 +45,7 @@ const ContentListItem = (content) => {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Button
|
||||
onClick={() => router.push(`${ isDraft ? '/draft' : '/details' }/${content.id}`)}
|
||||
onClick={handleClick}
|
||||
label="Open"
|
||||
outlined
|
||||
/>
|
||||
|
@ -56,6 +56,7 @@ export default function CourseDetails({ processedEvent }) {
|
||||
};
|
||||
|
||||
const fetchAuthor = useCallback(async (pubkey) => {
|
||||
if (!pubkey) return;
|
||||
const author = await ndk.getUser({ pubkey });
|
||||
const profile = await author.fetchProfile();
|
||||
const fields = await findKind0Fields(profile);
|
||||
|
191
src/components/course/DraftCourseDetails.js
Normal file
191
src/components/course/DraftCourseDetails.js
Normal file
@ -0,0 +1,191 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Button } from 'primereact/button';
|
||||
import Image from 'next/image';
|
||||
import dynamic from 'next/dynamic';
|
||||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { findKind0Fields } from '@/utils/nostr';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export default function DraftCourseDetails({ processedEvent, lessons }) {
|
||||
const [author, setAuthor] = useState(null);
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const ndk = useNDKContext();
|
||||
|
||||
const fetchAuthor = useCallback(async (pubkey) => {
|
||||
if (!pubkey) return;
|
||||
const author = await ndk.getUser({ pubkey });
|
||||
const profile = await author.fetchProfile();
|
||||
const fields = await findKind0Fields(profile);
|
||||
if (fields) {
|
||||
setAuthor(fields);
|
||||
}
|
||||
}, [ndk]);
|
||||
|
||||
useEffect(() => {
|
||||
if (processedEvent) {
|
||||
fetchAuthor(processedEvent?.user?.pubkey);
|
||||
}
|
||||
}, [fetchAuthor, processedEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
setUser(session.user);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('delete');
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newCourseId = uuidv4();
|
||||
const processedLessons = [];
|
||||
|
||||
try {
|
||||
// Step 1: Process lessons
|
||||
for (const lesson of lessons) {
|
||||
processedLessons.push({
|
||||
d: lesson?.d,
|
||||
kind: lesson?.price ? 30402 : 30023,
|
||||
pubkey: lesson.pubkey
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Create and publish course
|
||||
const courseEvent = createCourseEvent(newCourseId, processedEvent.title, processedEvent.summary, processedEvent.image, processedLessons);
|
||||
const published = await courseEvent.publish();
|
||||
|
||||
console.log('published', published);
|
||||
|
||||
if (!published) {
|
||||
throw new Error('Failed to publish course');
|
||||
}
|
||||
|
||||
// Step 3: Save course to db
|
||||
console.log('processedLessons:', processedLessons);
|
||||
await axios.post('/api/courses', {
|
||||
id: newCourseId,
|
||||
resources: {
|
||||
connect: processedLessons.map(lesson => ({ id: lesson?.d }))
|
||||
},
|
||||
noteId: courseEvent.id,
|
||||
user: {
|
||||
connect: { id: user.id }
|
||||
},
|
||||
price: processedEvent?.price || 0
|
||||
});
|
||||
|
||||
// step 4: Update all resources to have the course id
|
||||
await Promise.all(processedLessons.map(lesson => axios.put(`/api/resources/${lesson?.d}`, { courseId: newCourseId })));
|
||||
|
||||
// Step 5: Delete draft
|
||||
await axios.delete(`/api/courses/drafts/${processedEvent.id}`);
|
||||
|
||||
// Step 6: Show success message and redirect
|
||||
showToast('success', 'Course created successfully');
|
||||
router.push(`/course/${courseEvent.id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating course:', error);
|
||||
showToast('error', error.message || 'Failed to create course. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const createCourseEvent = (courseId, title, summary, coverImage, lessons) => {
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 30004;
|
||||
event.content = "";
|
||||
event.tags = [
|
||||
['d', courseId],
|
||||
['name', title],
|
||||
['picture', coverImage],
|
||||
['image', coverImage],
|
||||
['description', summary],
|
||||
['l', "Education"],
|
||||
...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]),
|
||||
];
|
||||
return event;
|
||||
};
|
||||
|
||||
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'>
|
||||
<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'>
|
||||
{processedEvent && processedEvent.topics && processedEvent.topics.length > 0 && (
|
||||
processedEvent.topics.map((topic, index) => (
|
||||
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{processedEvent?.title}</h1>
|
||||
<p className='text-xl mt-6'>{processedEvent?.summary}</p>
|
||||
<div className='flex flex-row w-full mt-6 items-center'>
|
||||
<Image
|
||||
alt="avatar thumbnail"
|
||||
src={returnImageProxy(author?.avatar, author?.pubkey)}
|
||||
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'>
|
||||
{author?.username || author?.name || author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
||||
{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"
|
||||
src={returnImageProxy(processedEvent.image)}
|
||||
width={344}
|
||||
height={194}
|
||||
className="w-[344px] h-full object-cover object-top rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
|
||||
<div className='w-fit flex flex-row justify-between'>
|
||||
<Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto m-2" />
|
||||
<Button onClick={() => router.push(`/draft/${draft?.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
|
||||
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[75vw] mx-auto mt-12 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]'>
|
||||
{
|
||||
processedEvent?.content && <MDDisplay source={processedEvent.content} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
71
src/components/course/DraftCourseLesson.js
Normal file
71
src/components/course/DraftCourseLesson.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Tag } from "primereact/tag";
|
||||
import Image from "next/image";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const DraftCourseLesson = ({ lesson, course }) => {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
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'>
|
||||
<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'>
|
||||
{lesson && lesson.topics && lesson.topics.length > 0 && (
|
||||
lesson.topics.map((topic, index) => (
|
||||
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{lesson?.title}</h1>
|
||||
<p className='text-xl mt-6'>{lesson?.summary}</p>
|
||||
<div className='flex flex-row w-full mt-6 items-center'>
|
||||
<Image
|
||||
alt="avatar thumbnail"
|
||||
src={returnImageProxy(lesson.author?.avatar, lesson.author?.pubkey)}
|
||||
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'>
|
||||
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
||||
{lesson && (
|
||||
<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"
|
||||
src={returnImageProxy(lesson.image)}
|
||||
width={344}
|
||||
height={194}
|
||||
className="w-[344px] h-full object-cover object-top rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[75vw] mx-auto mt-12 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]'>
|
||||
{
|
||||
lesson?.content && <MDDisplay source={lesson.content} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DraftCourseLesson;
|
@ -44,94 +44,84 @@ const CourseForm = () => {
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
/**
|
||||
* Course Creation Flow:
|
||||
* 1. Generate a new course ID
|
||||
* 2. Process each lesson:
|
||||
* - If unpublished: create event, publish to Nostr, save to DB, delete draft
|
||||
* - If published: use existing data
|
||||
* 3. Create and publish course event to Nostr
|
||||
* 4. Save course to database
|
||||
* 5. Show success message and redirect to course page
|
||||
*/
|
||||
useEffect(() => {
|
||||
console.log('selectedLessons:', selectedLessons);
|
||||
}, [selectedLessons]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
const handleDraftSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newCourseId = uuidv4();
|
||||
const processedLessons = [];
|
||||
// Prepare the lessons from selected lessons
|
||||
const resources = await Promise.all(selectedLessons.map(async (lesson) => {
|
||||
// if .type is present than this lesson is a draft we need to publish
|
||||
if (lesson?.type) {
|
||||
const event = createLessonEvent(lesson);
|
||||
const published = await event.publish();
|
||||
|
||||
if (!published) {
|
||||
throw new Error(`Failed to publish lesson: ${lesson.title}`);
|
||||
}
|
||||
|
||||
// Now post to resources
|
||||
const resource = await axios.post('/api/resources', {
|
||||
id: event.tags.find(tag => tag[0] === 'd')[1],
|
||||
userId: user.id,
|
||||
price: lesson.price || 0,
|
||||
noteId: event.id,
|
||||
});
|
||||
|
||||
if (resource.status !== 201) {
|
||||
throw new Error(`Failed to post resource: ${lesson.title}`);
|
||||
}
|
||||
|
||||
// now delete the draft
|
||||
const deleted = await axios.delete(`/api/drafts/${lesson.id}`);
|
||||
|
||||
if (deleted.status !== 204) {
|
||||
throw new Error(`Failed to delete draft: ${lesson.title}`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: lesson.id,
|
||||
userId: user.id,
|
||||
price: lesson.price || 0,
|
||||
noteId: event.id,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: lesson.d,
|
||||
userId: user.id,
|
||||
price: lesson.price || 0,
|
||||
noteId: lesson.id,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
console.log('resources:', resources);
|
||||
|
||||
const payload = {
|
||||
userId: user.id,
|
||||
title,
|
||||
summary,
|
||||
image: coverImage,
|
||||
price: price || 0,
|
||||
resources, // Send the array of lesson/resource IDs
|
||||
};
|
||||
|
||||
console.log('payload:', payload);
|
||||
|
||||
try {
|
||||
// Step 1: Process lessons
|
||||
for (const lesson of selectedLessons) {
|
||||
let noteId = lesson.noteId;
|
||||
// Post the course draft to the API
|
||||
const response = await axios.post('/api/courses/drafts', payload);
|
||||
|
||||
if (!lesson.published_at) {
|
||||
// Publish unpublished lesson
|
||||
const event = createLessonEvent(lesson);
|
||||
const published = await event.publish();
|
||||
|
||||
if (!published) {
|
||||
throw new Error(`Failed to publish lesson: ${lesson.title}`);
|
||||
}
|
||||
|
||||
noteId = event.id;
|
||||
|
||||
// Save to db and delete draft
|
||||
await Promise.all([
|
||||
axios.post('/api/resources', {
|
||||
id: lesson.id,
|
||||
noteId: noteId,
|
||||
userId: user.id,
|
||||
price: lesson.price || 0,
|
||||
}),
|
||||
axios.delete(`/api/drafts/${lesson.id}`)
|
||||
]);
|
||||
}
|
||||
// if the lesson was already published we will have d tag, otherwise we will have id
|
||||
// if the lesson was already published we will have kind tag, otherwise we will use price tag to determine the kind
|
||||
// if the lesson was already published we will have pubkey tag, otherwise we will use user.pubkey
|
||||
processedLessons.push({
|
||||
d: lesson?.d || lesson.id,
|
||||
kind: lesson.kind ?? (lesson.price ? 30402 : 30023),
|
||||
pubkey: lesson.pubkey || user.pubkey
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Create and publish course
|
||||
const courseEvent = createCourseEvent(newCourseId, title, summary, coverImage, processedLessons);
|
||||
const published = await courseEvent.publish();
|
||||
|
||||
console.log('published', published);
|
||||
|
||||
if (!published) {
|
||||
throw new Error('Failed to publish course');
|
||||
}
|
||||
|
||||
// Step 3: Save course to db
|
||||
console.log('processedLessons:', processedLessons);
|
||||
await axios.post('/api/courses', {
|
||||
id: newCourseId,
|
||||
resources: {
|
||||
connect: processedLessons.map(lesson => ({ id: lesson?.d }))
|
||||
},
|
||||
noteId: courseEvent.id,
|
||||
user: {
|
||||
connect: { id: user.id }
|
||||
},
|
||||
price: price || 0
|
||||
});
|
||||
|
||||
// step 4: Update all resources to have the course id
|
||||
await Promise.all(processedLessons.map(lesson => axios.put(`/api/resources/${lesson?.d}`, { courseId: newCourseId })));
|
||||
|
||||
// Step 5: Show success message and redirect
|
||||
showToast('success', 'Course created successfully');
|
||||
router.push(`/course/${courseEvent.id}`);
|
||||
console.log('response:', response);
|
||||
|
||||
// If successful, navigate to the course page
|
||||
showToast('success', 'Course draft saved successfully');
|
||||
router.push(`/course/${response.data.id}/draft`);
|
||||
} catch (error) {
|
||||
console.error('Error creating course:', error);
|
||||
showToast('error', error.message || 'Failed to create course. Please try again.');
|
||||
console.error('Error saving course draft:', error);
|
||||
showToast('error', 'Failed to save course draft. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
@ -150,22 +140,6 @@ const CourseForm = () => {
|
||||
return event;
|
||||
};
|
||||
|
||||
const createCourseEvent = (courseId, title, summary, coverImage, lessons) => {
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 30004;
|
||||
event.content = "";
|
||||
event.tags = [
|
||||
['d', courseId],
|
||||
['name', title],
|
||||
['picture', coverImage],
|
||||
['image', coverImage],
|
||||
['description', summary],
|
||||
['l', "Education"],
|
||||
...lessons.map((lesson) => ['a', `${lesson.kind}:${lesson.pubkey}:${lesson.d}`]),
|
||||
];
|
||||
return event;
|
||||
};
|
||||
|
||||
const handleLessonChange = (e, index) => {
|
||||
const selectedLessonId = e.value;
|
||||
const selectedLesson = getContentOptions(index).flatMap(group => group.items).find(lesson => lesson.value === selectedLessonId);
|
||||
@ -258,7 +232,7 @@ const CourseForm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleDraftSubmit}>
|
||||
<div className="p-inputgroup flex-1">
|
||||
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
||||
</div>
|
||||
|
@ -96,6 +96,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
||||
summary,
|
||||
price,
|
||||
content,
|
||||
d: draft.d,
|
||||
image: coverImage,
|
||||
topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'resource']
|
||||
}
|
||||
@ -112,8 +113,11 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
|
||||
const published = await ndk.publish(event);
|
||||
|
||||
if (published) {
|
||||
// update the resource with new noteId
|
||||
const response = await axios.put(`/api/resources/${draft.d}`, { noteId: event.id });
|
||||
console.log('response', response);
|
||||
showToast('success', 'Success', 'Resource published successfully.');
|
||||
router.push(`/resource/${event.id}`);
|
||||
router.push(`/details/${event.id}`);
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to publish resource. Please try again.');
|
||||
}
|
||||
|
58
src/db/models/courseDraftModels.js
Normal file
58
src/db/models/courseDraftModels.js
Normal file
@ -0,0 +1,58 @@
|
||||
import prisma from "@/db/prisma";
|
||||
|
||||
// Get all CourseDrafts for a specific user
|
||||
export const getAllCourseDraftsByUserId = async (userId) => {
|
||||
return await prisma.courseDraft.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
user: true, // Include the related user
|
||||
resources: true, // Include related resources
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Get a specific CourseDraft by its ID
|
||||
export const getCourseDraftById = async (id) => {
|
||||
return await prisma.courseDraft.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: true, // Include the related user
|
||||
resources: true, // Include related resources
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Create a new CourseDraft
|
||||
export const createCourseDraft = async (data) => {
|
||||
return await prisma.courseDraft.create({
|
||||
data: {
|
||||
...data,
|
||||
resources: {
|
||||
connect: data.resources.map((resource) => ({ id: resource.id })),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
resources: true,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Update an existing CourseDraft by its ID
|
||||
export const updateCourseDraft = async (id, data) => {
|
||||
return await prisma.courseDraft.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
resources: {
|
||||
set: data.resourceIds?.map((resourceId) => ({ id: resourceId })),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Delete a CourseDraft by its ID
|
||||
export const deleteCourseDraft = async (id) => {
|
||||
return await prisma.courseDraft.delete({
|
||||
where: { id },
|
||||
});
|
||||
};
|
@ -32,6 +32,24 @@ export async function isResourcePartOfAnyCourse(resourceId) {
|
||||
return courses.length > 0;
|
||||
}
|
||||
|
||||
export const updateLessonInCourse = async (courseId, resourceId, data) => {
|
||||
return await prisma.course.update({
|
||||
where: { id: courseId },
|
||||
data: {
|
||||
resources: {
|
||||
update: {
|
||||
where: { id: resourceId },
|
||||
data: {
|
||||
title: data.title,
|
||||
summary: data.summary,
|
||||
// Add any other fields you want to update in the lesson
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const createResource = async (data) => {
|
||||
return await prisma.resource.create({
|
||||
data,
|
||||
|
@ -51,6 +51,7 @@ export const createUser = async (data) => {
|
||||
};
|
||||
|
||||
export const updateUser = async (id, data) => {
|
||||
console.log("user modelllll", id, data)
|
||||
return await prisma.user.update({
|
||||
where: { id },
|
||||
data,
|
||||
|
@ -23,8 +23,8 @@ export function useContentIdsQuery() {
|
||||
const { data: contentIds, isLoading: contentIdsLoading, error: contentIdsError, refetch: refetchContentIds } = useQuery({
|
||||
queryKey: ['contentIds', isClient],
|
||||
queryFn: fetchContentIdsDB,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
// staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
// refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient
|
||||
});
|
||||
|
||||
|
@ -20,15 +20,31 @@ export function useDraftsQuery() {
|
||||
|
||||
const fetchDraftsDB = async () => {
|
||||
try {
|
||||
let allDrafts = [];
|
||||
if (!user.id) {
|
||||
return [];
|
||||
}
|
||||
const response = await axios.get(`/api/drafts/all/${user.id}`);
|
||||
if (response.status === 200) {
|
||||
allDrafts = response.data;
|
||||
const courseDrafts = await fetchCourseDrafts();
|
||||
allDrafts = [...allDrafts, ...courseDrafts];
|
||||
}
|
||||
return allDrafts;
|
||||
} catch (error) {
|
||||
console.error('Error fetching drafts from DB:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCourseDrafts = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/courses/drafts/${user.id}/all`);
|
||||
const drafts = response.data;
|
||||
console.log('drafts:', drafts);
|
||||
return drafts;
|
||||
} catch (error) {
|
||||
console.error('Error fetching drafts from DB:', error);
|
||||
console.error('Error fetching course drafts from DB:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@ -36,8 +52,8 @@ export function useDraftsQuery() {
|
||||
const { data: drafts, isLoading: draftsLoading, error: draftsError, refetch: refetchDrafts } = useQuery({
|
||||
queryKey: ['drafts', isClient],
|
||||
queryFn: fetchDraftsDB,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
// staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
// refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient && !!user.id, // Only enable if client-side and user ID is available
|
||||
});
|
||||
|
||||
|
@ -1,45 +1,33 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNDKContext } from '@/context/NDKContext';
|
||||
import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||
import axios from 'axios';
|
||||
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||
|
||||
export function useCoursesQuery() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||
const ndk = useNDKContext();
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refetchContentIds();
|
||||
}, [refetchContentIds]);
|
||||
|
||||
const hasRequiredProperties = (event) => {
|
||||
if (contentIdsLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasCourseTag = event.tags.some(([tag, value]) => tag === "t" && value === "course");
|
||||
const hasId = contentIds.includes(event.id);
|
||||
return hasCourseTag && hasId;
|
||||
const hasRequiredProperties = (event, contentIds) => {
|
||||
// currently no topic tag added
|
||||
// const hasCourseTag = event.tags.some(([tag, value]) => tag === "t" && value === "course");
|
||||
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
|
||||
return hasId;
|
||||
};
|
||||
|
||||
const fetchCoursesFromNDK = async () => {
|
||||
try {
|
||||
if (contentIdsLoading) {
|
||||
return []; // or a loading state indication
|
||||
}
|
||||
if (contentIdsError) {
|
||||
console.error('Error fetching content IDs:', contentIdsError);
|
||||
return [];
|
||||
}
|
||||
if (!contentIds) {
|
||||
return [];
|
||||
const response = await axios.get(`/api/content/all`);
|
||||
const contentIds = response.data;
|
||||
|
||||
if (!contentIds || contentIds.length === 0) {
|
||||
console.log('No content IDs found');
|
||||
return []; // Return early if no content IDs are found
|
||||
}
|
||||
|
||||
await ndk.connect();
|
||||
@ -47,9 +35,11 @@ export function useCoursesQuery() {
|
||||
const filter = { kinds: [30004], authors: [AUTHOR_PUBKEY] };
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
|
||||
console.log('events', events);
|
||||
|
||||
if (events && events.size > 0) {
|
||||
const eventsArray = Array.from(events);
|
||||
const courses = eventsArray.filter(event => hasRequiredProperties(event));
|
||||
const courses = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
return courses;
|
||||
}
|
||||
return [];
|
||||
@ -62,8 +52,8 @@ export function useCoursesQuery() {
|
||||
const { data: courses, isLoading: coursesLoading, error: coursesError, refetch: refetchCourses } = useQuery({
|
||||
queryKey: ['courses', isClient],
|
||||
queryFn: fetchCoursesFromNDK,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
// staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
// refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient,
|
||||
});
|
||||
|
||||
|
@ -1,73 +1,59 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNDKContext } from '@/context/NDKContext';
|
||||
import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||
import axios from 'axios';
|
||||
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||
|
||||
export function useResourcesQuery() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const ndk = useNDKContext();
|
||||
|
||||
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||
const ndk = useNDKContext();
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
const hasRequiredProperties = (event, contentIds) => {
|
||||
const hasPlebDevs = event.tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||
const hasResource = event.tags.some(([tag, value]) => tag === "t" && value === "resource");
|
||||
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
|
||||
return hasPlebDevs && hasResource && hasId;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetchContentIds();
|
||||
}, [refetchContentIds]);
|
||||
const fetchResourcesFromNDK = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/content/all`);
|
||||
const contentIds = response.data;
|
||||
|
||||
const hasRequiredProperties = (event) => {
|
||||
if (!contentIds) {
|
||||
return false;
|
||||
}
|
||||
if (!contentIds || contentIds.length === 0) {
|
||||
console.log('No content IDs found');
|
||||
return []; // Return early if no content IDs are found
|
||||
}
|
||||
|
||||
const hasPlebDevs = event.tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||
const hasWorkshop = event.tags.some(([tag, value]) => tag === "t" && value === "resource");
|
||||
const hasId = contentIds.includes(event.id);
|
||||
return hasPlebDevs && hasWorkshop && hasId;
|
||||
};
|
||||
await ndk.connect();
|
||||
|
||||
const fetchResourcesFromNDK = async () => {
|
||||
try {
|
||||
if (contentIdsLoading) {
|
||||
return []; // or a loading state indication
|
||||
}
|
||||
if (contentIdsError) {
|
||||
console.error('Error fetching content IDs:', contentIdsError);
|
||||
return [];
|
||||
}
|
||||
if (!contentIds) {
|
||||
return [];
|
||||
}
|
||||
console.log('Fetching workshops from NDK');
|
||||
await ndk.connect();
|
||||
const filter = { kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] };
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
|
||||
const filter = { kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] };
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
if (events && events.size > 0) {
|
||||
const eventsArray = Array.from(events);
|
||||
const resources = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
return resources;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching resources from NDK:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
if (events && events.size > 0) {
|
||||
const eventsArray = Array.from(events);
|
||||
console.log('eventsArray', eventsArray)
|
||||
const resources = eventsArray.filter(event => hasRequiredProperties(event));
|
||||
return resources;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching workshops from NDK:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const { data: resources, isLoading: resourcesLoading, error: resourcesError, refetch: refetchResources } = useQuery({
|
||||
queryKey: ['resources', isClient],
|
||||
queryFn: fetchResourcesFromNDK,
|
||||
// staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
// refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient,
|
||||
});
|
||||
|
||||
const { data: resources, isLoading: resourcesLoading, error: resourcesError, refetch: refetchResources } = useQuery({
|
||||
queryKey: ['resources', isClient],
|
||||
queryFn: fetchResourcesFromNDK,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient,
|
||||
})
|
||||
|
||||
return { resources, resourcesLoading, resourcesError, refetchResources }
|
||||
}
|
||||
return { resources, resourcesLoading, resourcesError, refetchResources };
|
||||
}
|
||||
|
@ -1,47 +1,35 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNDKContext } from '@/context/NDKContext';
|
||||
import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||
import axios from 'axios';
|
||||
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||
|
||||
export function useWorkshopsQuery() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||
const ndk = useNDKContext();
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refetchContentIds();
|
||||
}, [refetchContentIds]);
|
||||
|
||||
const hasRequiredProperties = (event) => {
|
||||
if (contentIdsLoading) {
|
||||
return false;
|
||||
}
|
||||
const hasRequiredProperties = (event, contentIds) => {
|
||||
const hasPlebDevs = event.tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||
const hasWorkshop = event.tags.some(([tag, value]) => tag === "t" && value === "workshop");
|
||||
const hasId = contentIds.includes(event.id);
|
||||
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
|
||||
return hasPlebDevs && hasWorkshop && hasId;
|
||||
};
|
||||
|
||||
const fetchWorkshopsFromNDK = async () => {
|
||||
try {
|
||||
if (contentIdsLoading) {
|
||||
return []; // or a loading state indication
|
||||
const response = await axios.get(`/api/content/all`);
|
||||
const contentIds = response.data;
|
||||
|
||||
if (!contentIds || contentIds.length === 0) {
|
||||
console.log('No content IDs found');
|
||||
return []; // Return early if no content IDs are found
|
||||
}
|
||||
if (contentIdsError) {
|
||||
console.error('Error fetching content IDs:', contentIdsError);
|
||||
return [];
|
||||
}
|
||||
if (!contentIds) {
|
||||
return [];
|
||||
}
|
||||
console.log('Fetching workshops from NDK');
|
||||
|
||||
await ndk.connect();
|
||||
|
||||
const filter = { kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] };
|
||||
@ -49,8 +37,7 @@ export function useWorkshopsQuery() {
|
||||
|
||||
if (events && events.size > 0) {
|
||||
const eventsArray = Array.from(events);
|
||||
console.log('eventsArray', eventsArray);
|
||||
const workshops = eventsArray.filter(event => hasRequiredProperties(event));
|
||||
const workshops = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
return workshops;
|
||||
}
|
||||
return [];
|
||||
@ -63,8 +50,8 @@ export function useWorkshopsQuery() {
|
||||
const { data: workshops, isLoading: workshopsLoading, error: workshopsError, refetch: refetchWorkshops } = useQuery({
|
||||
queryKey: ['workshops', isClient],
|
||||
queryFn: fetchWorkshopsFromNDK,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
// staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
// refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient,
|
||||
});
|
||||
|
||||
|
@ -41,7 +41,14 @@ export default NextAuth({
|
||||
const response = await axios.get(`${BASE_URL}/api/users/${credentials.pubkey}`);
|
||||
if (response.status === 200 && response.data) {
|
||||
const fields = await findKind0Fields(profile);
|
||||
return { pubkey: credentials.pubkey, ...fields };
|
||||
|
||||
// Combine user object with kind0Fields, giving priority to kind0Fields
|
||||
const combinedUser = { ...fields, ...response.data };
|
||||
|
||||
// Update the user on the backend if necessary
|
||||
// await axios.put(`${BASE_URL}/api/users/${combinedUser.id}`, combinedUser);
|
||||
|
||||
return combinedUser;
|
||||
} else if (response.status === 204) {
|
||||
// Create user
|
||||
if (profile) {
|
||||
@ -63,7 +70,7 @@ export default NextAuth({
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
// Add user to the token if user object exists
|
||||
// Add combined user object to the token
|
||||
if (user) {
|
||||
token.user = user;
|
||||
}
|
||||
|
56
src/pages/api/courses/drafts/[slug].js
Normal file
56
src/pages/api/courses/drafts/[slug].js
Normal file
@ -0,0 +1,56 @@
|
||||
import { getAllCourseDraftsByUserId, getCourseDraftById, updateCourseDraft, deleteCourseDraft } from "@/db/models/courseDraftModels";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { slug } = req.query;
|
||||
console.log('slug:', slug);
|
||||
const userId = req.body?.userId || req.query?.userId;
|
||||
console.log('userId:', userId);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (slug && !userId) {
|
||||
try {
|
||||
const courseDraft = await getCourseDraftById(slug);
|
||||
if (courseDraft) {
|
||||
res.status(200).json(courseDraft);
|
||||
} else {
|
||||
res.status(404).json({ error: 'Course draft not found' });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
} else if (userId) {
|
||||
try {
|
||||
console.log('INHEEEERE:', userId);
|
||||
const courseDrafts = await getAllCourseDraftsByUserId(userId);
|
||||
res.status(200).json(courseDrafts);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
} else {
|
||||
res.status(400).json({ error: 'User ID is required' });
|
||||
}
|
||||
} else if (req.method === 'PUT') {
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: 'Slug is required to update a course draft' });
|
||||
}
|
||||
try {
|
||||
const updatedCourseDraft = await updateCourseDraft(slug, req.body);
|
||||
res.status(200).json(updatedCourseDraft);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
} else if (req.method === 'DELETE') {
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: 'Slug is required to delete a course draft' });
|
||||
}
|
||||
try {
|
||||
await deleteCourseDraft(slug);
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
} else {
|
||||
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
|
||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||
}
|
||||
}
|
20
src/pages/api/courses/drafts/[slug]/all.js
Normal file
20
src/pages/api/courses/drafts/[slug]/all.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { getAllCourseDraftsByUserId } from "@/db/models/courseDraftModels";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
// the slug here is user id to get all drafts for a given user
|
||||
const {slug} = req.query;
|
||||
if (req.method === 'GET') {
|
||||
if (slug) {
|
||||
try {
|
||||
const courseDrafts = await getAllCourseDraftsByUserId(slug);
|
||||
res.status(200).json(courseDrafts);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
} else {
|
||||
res.status(400).json({ error: 'User ID is required' });
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
}
|
18
src/pages/api/courses/drafts/index.js
Normal file
18
src/pages/api/courses/drafts/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { createCourseDraft } from "@/db/models/courseDraftModels";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
if (!req.body || !req.body.userId) {
|
||||
return res.status(400).json({ error: 'User ID is required' });
|
||||
}
|
||||
|
||||
const newCourseDraft = await createCourseDraft(req.body);
|
||||
res.status(201).json(newCourseDraft);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ export default async function handler(req, res) {
|
||||
case 'PUT':
|
||||
if (!isPubkey) {
|
||||
// Update operation should be done with an ID, not a pubkey
|
||||
const updatedUser = await updateUser(parseInt(slug), req.body);
|
||||
const updatedUser = await updateUser(slug, req.body);
|
||||
res.status(200).json(updatedUser);
|
||||
} else {
|
||||
// Handle attempt to update user with pubkey
|
||||
|
@ -11,8 +11,6 @@ export default function SignIn() {
|
||||
|
||||
const { data: session, status } = useSession(); // Get the current session's data and status
|
||||
|
||||
// const ndk = useNDKContext()
|
||||
|
||||
useEffect(() => {
|
||||
console.log("session", session)
|
||||
}, [session])
|
||||
@ -33,7 +31,6 @@ export default function SignIn() {
|
||||
try {
|
||||
const user = await nip07signer.user()
|
||||
|
||||
console.log("user in signin", user)
|
||||
const pubkey = user?._pubkey
|
||||
signIn("nostr", { pubkey })
|
||||
} catch (error) {
|
||||
|
90
src/pages/course/[slug]/draft.js
Normal file
90
src/pages/course/[slug]/draft.js
Normal file
@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import axios from "axios";
|
||||
import { parseEvent, findKind0Fields } from "@/utils/nostr";
|
||||
import DraftCourseDetails from "@/components/course/DraftCourseDetails";
|
||||
import DraftCourseLesson from "@/components/course/DraftCourseLesson";
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const DraftCourse = () => {
|
||||
const [course, setCourse] = useState(null);
|
||||
const [lessons, setLessons] = useState([]);
|
||||
const [lessonsWithAuthors, setLessonsWithAuthors] = useState([]);
|
||||
|
||||
const router = useRouter();
|
||||
const ndk = useNDKContext();
|
||||
|
||||
const fetchAuthor = useCallback(async (pubkey) => {
|
||||
if (!pubkey) return;
|
||||
const author = await ndk.getUser({ pubkey });
|
||||
const profile = await author.fetchProfile();
|
||||
const fields = await findKind0Fields(profile);
|
||||
if (fields) {
|
||||
return fields;
|
||||
}
|
||||
return null; // Return null if no fields found
|
||||
}, [ndk]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
const { slug } = router.query;
|
||||
|
||||
axios.get(`/api/courses/drafts/${slug}`)
|
||||
.then(res => {
|
||||
console.log('res:', res.data);
|
||||
setCourse(res.data);
|
||||
setLessons(res.data.resources); // Set the raw lessons
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, [router.isReady, router.query]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLessonDetails = async () => {
|
||||
if (lessons.length > 0) {
|
||||
await ndk.connect();
|
||||
|
||||
const newLessonsWithAuthors = await Promise.all(lessons.map(async (lesson) => {
|
||||
const filter = {
|
||||
"#d": [lesson.id]
|
||||
};
|
||||
|
||||
const event = await ndk.fetchEvent(filter);
|
||||
if (event) {
|
||||
const author = await fetchAuthor(event.pubkey);
|
||||
return {
|
||||
...parseEvent(event),
|
||||
author
|
||||
};
|
||||
}
|
||||
return lesson; // Fallback to the original lesson if no event found
|
||||
}));
|
||||
|
||||
setLessonsWithAuthors(newLessonsWithAuthors);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLessonDetails();
|
||||
}, [lessons, ndk, fetchAuthor]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DraftCourseDetails processedEvent={course} lessons={lessonsWithAuthors} />
|
||||
{lessonsWithAuthors.length > 0 && lessonsWithAuthors.map((lesson, index) => (
|
||||
<DraftCourseLesson key={lesson.id} lesson={lesson} course={course} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DraftCourse;
|
@ -16,6 +16,7 @@ import { useToast } from '@/hooks/useToast';
|
||||
import { useNDKContext } from '@/context/NDKContext';
|
||||
import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription';
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{
|
||||
@ -31,6 +32,7 @@ const BitcoinConnectPayButton = dynamic(
|
||||
);
|
||||
|
||||
const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY;
|
||||
const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY;
|
||||
|
||||
export default function Details() {
|
||||
const [event, setEvent] = useState(null);
|
||||
@ -54,6 +56,7 @@ export default function Details() {
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
console.log('session:', session);
|
||||
setUser(session.user);
|
||||
}
|
||||
}, [session]);
|
||||
@ -77,17 +80,14 @@ export default function Details() {
|
||||
useEffect(() => {
|
||||
const decryptContent = async () => {
|
||||
if (user && paidResource) {
|
||||
if (!user.purchased.includes(processedEvent.id)) {
|
||||
// decrypt the content
|
||||
console.log('privkey', privkey);
|
||||
console.log('user.pubkey', user.pubkey);
|
||||
console.log('processedEvent.content', processedEvent.content);
|
||||
const decryptedContent = await nip04.decrypt(privkey, user.pubkey, processedEvent.content);
|
||||
console.log('decryptedContent', decryptedContent);
|
||||
setDecryptedContent(decryptedContent);
|
||||
if (user.purchased.includes(processedEvent.id) || (user?.role && user?.role.subscribed)) {
|
||||
// decrypt the content
|
||||
const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content);
|
||||
console.log('decryptedContent', decryptedContent);
|
||||
setDecryptedContent(decryptedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
decryptContent();
|
||||
}, [user, paidResource, processedEvent]);
|
||||
|
||||
@ -181,7 +181,7 @@ export default function Details() {
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
const response = await axios.delete(`/api/resources/${processedEvent.id}`);
|
||||
const response = await axios.delete(`/api/resources/${processedEvent.d}`);
|
||||
if (response.status === 204) {
|
||||
showToast('success', 'Success', 'Resource deleted successfully.');
|
||||
router.push('/');
|
||||
@ -274,9 +274,13 @@ export default function Details() {
|
||||
)}
|
||||
<div className='w-[75vw] mx-auto mt-12 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]'>
|
||||
{
|
||||
processedEvent?.content && <MDDisplay source={processedEvent.content} />
|
||||
decryptedContent ? (
|
||||
<MDDisplay source={decryptedContent} />
|
||||
) : (
|
||||
processedEvent?.content && <MDDisplay source={processedEvent.content} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ export default function Draft() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('unsignedEvent:', unsignedEvent.validate());
|
||||
console.log('unsignedEvent:', unsignedEvent.validate(), unsignedEvent);
|
||||
console.log('unsignedEvent validation:', validationResult);
|
||||
|
||||
if (unsignedEvent) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
export const findKind0Fields = async (kind0) => {
|
||||
console.log('kind0', kind0);
|
||||
let fields = {}
|
||||
|
||||
const usernameProperties = ['name', 'displayName', 'display_name', 'username', 'handle', 'alias'];
|
||||
|
Loading…
x
Reference in New Issue
Block a user