diff --git a/src/components/forms/CourseForm.js b/src/components/forms/CourseForm.js index 00286cf..9d6d6c8 100644 --- a/src/components/forms/CourseForm.js +++ b/src/components/forms/CourseForm.js @@ -8,7 +8,7 @@ import { Dropdown } from "primereact/dropdown"; import { v4 as uuidv4, v4 } from 'uuid'; import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage"; import { useNostr } from "@/hooks/useNostr"; -import {nip19} from "nostr-tools" +import { nip19 } from "nostr-tools" import { parseEvent } from "@/utils/nostr"; import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem"; import 'primeicons/primeicons.css'; @@ -119,12 +119,12 @@ const CourseForm = () => { if (published) { // delete the draft axios.delete(`/api/drafts/${lesson.id}`) - .then((response) => { - console.log('Draft deleted:', response); - }) - .catch((error) => { - console.error('Error deleting draft:', error); - }); + .then((response) => { + console.log('Draft deleted:', response); + }) + .catch((error) => { + console.error('Error deleting draft:', error); + }); } } } @@ -178,17 +178,22 @@ const CourseForm = () => { const signedCourseEvent = await window?.nostr?.signEvent(courseEvent); console.log('signedCourseEvent:', signedCourseEvent); // Publish the course event using Nostr - await publish(signedCourseEvent); + const published = await publish(signedCourseEvent); - // Reset the form fields after publishing the course - setTitle(''); - setSummary(''); - setChecked(false); - setPrice(0); - setCoverImage(''); - setLessons([{ id: uuidv4(), title: 'Select a lesson' }]); - setSelectedLessons([]); - setTopics(['']); + if (published) { + + // Reset the form fields after publishing the course + setTitle(''); + setSummary(''); + setChecked(false); + setPrice(0); + setCoverImage(''); + setLessons([{ id: uuidv4(), title: 'Select a lesson' }]); + setSelectedLessons([]); + setTopics(['']); + } else { + // Handle error + } } }; diff --git a/src/db/models/genericModels.js b/src/db/models/genericModels.js new file mode 100644 index 0000000..bbb8e97 --- /dev/null +++ b/src/db/models/genericModels.js @@ -0,0 +1,19 @@ +import prisma from "../prisma"; + +export const getAllContentIds = async () => { + const courseIds = await prisma.course.findMany({ + select: { + id: true, + }, + }); + + const resourceIds = await prisma.resource.findMany({ + select: { + id: true, + }, + }); + + const combinedIds = [...courseIds, ...resourceIds].map((item) => item.id); + + return combinedIds; + }; \ No newline at end of file diff --git a/src/db/prisma.js b/src/db/prisma.js index 4e5830c..0dfec71 100644 --- a/src/db/prisma.js +++ b/src/db/prisma.js @@ -1,28 +1,17 @@ -// db/prisma.js - -// Import the PrismaClient class from the @prisma/client package. -import { PrismaClient } from '@prisma/client'; +const { PrismaClient } = require('@prisma/client'); // Declare a variable to hold our Prisma client instance. let 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; +// Check if there is already an instance of PrismaClient attached to the global object. +// If not, create a new instance of PrismaClient and attach it to the global object. +// This ensures that the same instance of PrismaClient is reused across multiple invocations. +if (!global.prisma) { + global.prisma = new PrismaClient(); } +// Assign the global PrismaClient instance to the prisma variable. +prisma = global.prisma; + // Export the prisma client instance, making it available for import in other parts of the application. -export default prisma; +module.exports = prisma; diff --git a/src/hooks/useLogin.js b/src/hooks/useLogin.js index 6a92b42..7fc8e42 100644 --- a/src/hooks/useLogin.js +++ b/src/hooks/useLogin.js @@ -61,42 +61,46 @@ export const useLogin = () => { const nostrLogin = useCallback(async () => { if (!window || !window.nostr) { - showToast('error', 'Nostr Unavailable', 'Nostr is not available'); - return; + showToast('error', 'Nostr Unavailable', 'Nostr is not available'); + return; } - + const publicKey = await window.nostr.getPublicKey(); if (!publicKey) { - alert('Failed to obtain public key'); - return; + showToast('error', 'Public Key Error', 'Failed to obtain public key'); + return; } - + try { - const response = await axios.get(`/api/users/${publicKey}`); - if (response.status !== 200) throw new Error('User not found'); -; - window.localStorage.setItem('user', JSON.stringify(response.data)); - router.push('/').then(() => window.location.reload()); - } catch (error) { + const response = await axios.get(`/api/users/${publicKey}`); + let userData; + + if (response.status === 204) { // User not found, create a new user const kind0 = await fetchKind0(publicKey); - const fields = await findKind0Fields(kind0); - const payload = { pubkey: publicKey, ...fields }; - - try { - const createUserResponse = await axios.post(`/api/users`, payload); - if (createUserResponse.status === 201) { - window.localStorage.setItem('user', JSON.stringify(createUserResponse.data)); - router.push('/').then(() => window.location.reload()); - } else { - console.error('Error creating user:', createUserResponse); - } - } catch (createError) { - console.error('Error creating user:', createError); - showToast('error', 'Error Creating User', 'Failed to create user'); + + let fields = {}; + if (kind0) { + fields = await findKind0Fields(kind0); } + + const payload = { pubkey: publicKey, ...fields }; + const createUserResponse = await axios.post(`/api/users`, payload); + if (createUserResponse.status !== 201) { + throw new Error('Failed to create user'); + } + userData = createUserResponse.data; + } else { + userData = response.data; + } + + window.localStorage.setItem('user', JSON.stringify(userData)); + router.push('/').then(() => window.location.reload()); + } catch (error) { + console.error('Error during login:', error); + showToast('error', 'Login Error', error.message || 'Failed to log in'); } - }, [router, showToast, fetchKind0]); + }, [router, showToast, fetchKind0]); const anonymousLogin = useCallback(() => { try { diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index 338e623..fbf9d47 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -14,6 +14,8 @@ const defaultRelays = [ "wss://relay.primal.net/" ]; +const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY; + export function useNostr() { const pool = useContext(NostrContext); const subscriptionQueue = useRef([]); @@ -175,12 +177,12 @@ export function useNostr() { aTagsAlt.push(`${event.kind}:${event.pubkey}:${event.d}`); eTags.push(event.id); }); - + // Create filters for batch querying const filterA = { kinds: [9735], '#a': aTags }; const filterE = { kinds: [9735], '#e': eTags }; const filterAAlt = { kinds: [9735], '#a': aTagsAlt }; - + // Perform batch queries // const [zapsA, zapsE] = await Promise.all([ // pool.querySync(defaultRelays, filterA), @@ -212,7 +214,7 @@ export function useNostr() { return []; } }; - + return new Promise((resolve) => { querySyncQueue.current.push(async () => { const zaps = await querySyncFn(); @@ -222,32 +224,33 @@ export function useNostr() { }); }, [pool, processQuerySyncQueue] - ); + ); const fetchKind0 = useCallback( async (publicKey) => { - try { - const kind0 = await new Promise((resolve, reject) => { - subscribe( - [{ authors: [publicKey], kinds: [0] }], - { - onevent: (event) => { - resolve(JSON.parse(event.content)); - }, - onerror: (error) => { - reject(error); - }, - } - ); - }); - return kind0; - } catch (error) { - console.error('Failed to fetch kind 0 for event:', error); - return []; - } + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve(null); // Resolve with null if no event is received within the timeout + }, 10000); // 10 seconds timeout + + subscribe( + [{ authors: [publicKey], kinds: [0] }], + { + onevent: (event) => { + clearTimeout(timeout); + resolve(JSON.parse(event.content)); + }, + onerror: (error) => { + clearTimeout(timeout); + console.error('Error fetching kind 0:', error); + resolve(null); + }, + } + ); + }); }, [subscribe] - ); + ); const zapEvent = useCallback( async (event, amount, comment) => { @@ -334,7 +337,7 @@ export function useNostr() { ); const fetchResources = useCallback(async () => { - const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }]; const hasRequiredTags = (tags) => { const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); // Check if 'resource' tag exists @@ -374,12 +377,12 @@ export function useNostr() { }, [subscribe]); const fetchWorkshops = useCallback(async () => { - const filter = [{ kinds: [30023, 30402], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const filter = [{ kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] }]; const hasRequiredTags = (tags) => { const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop"); - + return hasPlebDevs && hasWorkshop; }; @@ -414,7 +417,7 @@ export function useNostr() { }, [subscribe]); const fetchCourses = useCallback(async () => { - const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; const hasRequiredTags = (tags) => { const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs"); @@ -453,5 +456,94 @@ export function useNostr() { }); }, [subscribe]); - return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents }; + const publishResource = useCallback( + async (resourceEvent) => { + const published = await publish(resourceEvent); + + if (published) { + const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resourceEvent); + + const user = window.localStorage.getItem('user'); + const userId = JSON.parse(user).id; + + const payload = { + + }; + + if (payload && payload.user) { + try { + const response = await axios.post('/api/resources', payload); + + if (response.status === 201) { + try { + const deleteResponse = await axios.delete(`/api/drafts/${resourceEvent.id}`); + + if (deleteResponse.status === 204) { + return true; + } + } catch (error) { + console.error('Error deleting draft:', error); + return false; + } + } + } catch (error) { + console.error('Error creating resource:', error); + return false; + } + } + } + + return false; + }, + [publish] + ); + + + const publishCourse = useCallback( + async (courseEvent) => { + const published = await publish(courseEvent); + + if (published) { + const user = window.localStorage.getItem('user'); + const pubkey = JSON.parse(user).pubkey; + + const payload = { + title: courseEvent.title, + summary: courseEvent.summary, + type: 'course', + content: courseEvent.content, + image: courseEvent.image, + user: pubkey, + topics: [...courseEvent.topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'course'] + }; + + if (payload && payload.user) { + try { + const response = await axios.post('/api/courses', payload); + + if (response.status === 201) { + try { + const deleteResponse = await axios.delete(`/api/drafts/${courseEvent.id}`); + + if (deleteResponse.status === 204) { + return true; + } + } catch (error) { + console.error('Error deleting draft:', error); + return false; + } + } + } catch (error) { + console.error('Error creating course:', error); + return false; + } + } + } + + return false; + }, + [publish] + ); + + return { subscribe, publish, fetchSingleEvent, fetchZapsForEvent, fetchKind0, fetchResources, fetchWorkshops, fetchCourses, zapEvent, fetchZapsForEvents, publishResource, publishCourse }; } \ No newline at end of file diff --git a/src/hooks/useNostrOld.js b/src/hooks/useNostrOld.js index 6294fb7..77db210 100644 --- a/src/hooks/useNostrOld.js +++ b/src/hooks/useNostrOld.js @@ -13,6 +13,8 @@ const initialRelays = [ "wss://relay.primal.net/" ]; +const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY; + export const useNostr = () => { const [relays, setRelays] = useState(initialRelays); const [relayStatuses, setRelayStatuses] = useState({}); @@ -159,7 +161,7 @@ export const useNostr = () => { // Fetch resources, workshops, courses, and streams with appropriate filters and update functions const fetchResources = async () => { - const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; const hasRequiredTags = async (eventData) => { const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource"); @@ -181,7 +183,7 @@ export const useNostr = () => { }; const fetchWorkshops = async () => { - const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; const hasRequiredTags = async (eventData) => { const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop"); @@ -203,7 +205,7 @@ export const useNostr = () => { }; const fetchCourses = async () => { - const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }]; + const filter = [{ kinds: [30023], authors: [AUTHOR_PUBKEY] }]; const hasRequiredTags = async (eventData) => { const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course"); @@ -226,7 +228,7 @@ export const useNostr = () => { }; // const fetchStreams = () => { - // const filter = [{kinds: [30311], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}]; + // const filter = [{kinds: [30311], authors: [AUTHOR_PUBKEY]}]; // const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs"); // fetchEvents(filter, 'streams', hasRequiredTags); // } diff --git a/src/pages/api/content/all.js b/src/pages/api/content/all.js new file mode 100644 index 0000000..ef395b4 --- /dev/null +++ b/src/pages/api/content/all.js @@ -0,0 +1,16 @@ +import { getAllContentIds } from '../../../db/models/genericModels'; + +export default async function handler(req, res) { + if (req.method === 'GET') { + try { + const ids = await getAllContentIds(); + res.status(200).json(ids); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } else { + // Handle any other HTTP method + res.setHeader('Allow', ['GET', 'POST']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/src/pages/api/users/[slug].js b/src/pages/api/users/[slug].js index e2634f2..34c2811 100644 --- a/src/pages/api/users/[slug].js +++ b/src/pages/api/users/[slug].js @@ -2,7 +2,6 @@ import { getUserById, getUserByPubkey, updateUser, deleteUser } from "@/db/model export default async function handler(req, res) { const { slug } = req.query; - // Determine if slug is a pubkey or an ID const isPubkey = /^[0-9a-fA-F]{64}$/.test(slug); @@ -15,44 +14,46 @@ export default async function handler(req, res) { // Assume slug is an ID const id = parseInt(slug); if (isNaN(id)) { - return res.status(400).json({ error: "Invalid identifier" }); + res.status(400).json({ error: "Invalid identifier" }); + return; } user = await getUserById(id); } if (!user) { - return res.status(204).end(); + res.status(204).end(); + return; } switch (req.method) { case 'GET': - return res.status(200).json(user); - + res.status(200).json(user); + break; case 'PUT': if (!isPubkey) { // Update operation should be done with an ID, not a pubkey const updatedUser = await updateUser(parseInt(slug), req.body); - return res.status(200).json(updatedUser); + res.status(200).json(updatedUser); } else { // Handle attempt to update user with pubkey - return res.status(400).json({ error: "Cannot update user with pubkey. Use ID instead." }); + res.status(400).json({ error: "Cannot update user with pubkey. Use ID instead." }); } - + break; case 'DELETE': if (!isPubkey) { // Delete operation should be done with an ID, not a pubkey await deleteUser(parseInt(slug)); - return res.status(204).end(); + res.status(204).end(); } else { // Handle attempt to delete user with pubkey - return res.status(400).json({ error: "Cannot delete user with pubkey. Use ID instead." }); + res.status(400).json({ error: "Cannot delete user with pubkey. Use ID instead." }); } - + break; default: res.setHeader('Allow', ['GET', 'PUT', 'DELETE']); - return res.status(405).end(`Method ${req.method} Not Allowed`); + res.status(405).end(`Method ${req.method} Not Allowed`); } } catch (error) { - return res.status(500).json({ error: error.message }); + res.status(500).json({ error: error.message }); } -} +} \ No newline at end of file diff --git a/src/pages/draft/[slug].js b/src/pages/draft/[slug].js index d746934..88c8929 100644 --- a/src/pages/draft/[slug].js +++ b/src/pages/draft/[slug].js @@ -32,7 +32,7 @@ export default function Details() { const { returnImageProxy } = useImageProxy(); - const { publish, fetchSingleEvent } = useNostr(); + const { publishCourse, publishResource, fetchSingleEvent } = useNostr(); const [user] = useLocalStorageWithEffect('user', {}); @@ -109,18 +109,25 @@ export default function Details() { return; } - await publish(signedEvent); + let published; - // check if the event is published - const publishedEvent = await fetchSingleEvent(signedEvent.id); + if (type === 'resource' || type === 'workshop') { + published = await publishResource(signedEvent); + } else if (type === 'course') { + published = await publishCourse(signedEvent); + } - console.log('publishedEvent:', publishedEvent); - - if (publishedEvent) { - // show success message - showToast('success', 'Success', `${type} published successfully.`); - // delete the draft - await axios.delete(`/api/drafts/${draft.id}`) + if (published) { + // check if the event is published + const publishedEvent = await fetchSingleEvent(signedEvent.id); + + console.log('publishedEvent:', publishedEvent); + + if (publishedEvent) { + // show success message + showToast('success', 'Success', `${type} published successfully.`); + // delete the draft + await axios.delete(`/api/drafts/${draft.id}`) .then(res => { if (res.status === 204) { showToast('success', 'Success', 'Draft deleted successfully.'); @@ -132,6 +139,7 @@ export default function Details() { .catch(err => { console.error(err); }); + } } } diff --git a/src/pages/index.js b/src/pages/index.js index 782ed73..fdf13cf 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,11 +1,30 @@ -import Head from 'next/head' -import React from 'react'; -import CoursesCarousel from '@/components/content/carousels/CoursesCarousel' -import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel' +import Head from 'next/head'; +import React, { useEffect } from 'react'; +import CoursesCarousel from '@/components/content/carousels/CoursesCarousel'; +import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel'; import HeroBanner from '@/components/banner/HeroBanner'; import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel'; +import { useLocalStorageWithEffect } from '@/hooks/useLocalStorage'; +import axios from 'axios'; export default function Home() { + const [contentIds, setContentIds] = useLocalStorageWithEffect('contentIds', []); + + // this is ready so now we can pass all ids into fetch hooks from loacl storage + useEffect(() => { + const fetchContentIds = async () => { + try { + const response = await axios.get('/api/content/all'); + const ids = response.data; + setContentIds(ids); + } catch (error) { + console.error('Failed to fetch content IDs:', error); + } + }; + + fetchContentIds(); + }, [setContentIds]); + return ( <>
@@ -21,5 +40,5 @@ export default function Home() {