mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 01:02:04 +00:00
A bunch of good stuff
This commit is contained in:
parent
fea5a7b76c
commit
80edbf0905
@ -7,6 +7,8 @@ import { Button } from "primereact/button";
|
|||||||
import { useRouter } from "next/router";;
|
import { useRouter } from "next/router";;
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
const MDEditor = dynamic(
|
const MDEditor = dynamic(
|
||||||
() => import("@uiw/react-md-editor"),
|
() => import("@uiw/react-md-editor"),
|
||||||
@ -16,7 +18,7 @@ const MDEditor = dynamic(
|
|||||||
);
|
);
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
const ResourceForm = ({ draft = null }) => {
|
const ResourceForm = ({ draft = null, isPublished = false }) => {
|
||||||
const [title, setTitle] = useState(draft?.title || '');
|
const [title, setTitle] = useState(draft?.title || '');
|
||||||
const [summary, setSummary] = useState(draft?.summary || '');
|
const [summary, setSummary] = useState(draft?.summary || '');
|
||||||
const [isPaidResource, setIsPaidResource] = useState(draft?.price ? true : false);
|
const [isPaidResource, setIsPaidResource] = useState(draft?.price ? true : false);
|
||||||
@ -29,6 +31,12 @@ const ResourceForm = ({ draft = null }) => {
|
|||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const ndk = useNDKContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('isPublished', isPublished);
|
||||||
|
console.log('draft', draft);
|
||||||
|
}, [isPublished, draft]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (session) {
|
||||||
@ -52,6 +60,69 @@ const ResourceForm = ({ draft = null }) => {
|
|||||||
}
|
}
|
||||||
}, [draft]);
|
}, [draft]);
|
||||||
|
|
||||||
|
const buildEvent = async (draft) => {
|
||||||
|
const dTag = draft.d
|
||||||
|
const event = new NDKEvent(ndk);
|
||||||
|
let encryptedContent;
|
||||||
|
|
||||||
|
if (draft?.price) {
|
||||||
|
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
||||||
|
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.kind = draft?.price ? 30402 : 30023; // Determine kind based on if price is present
|
||||||
|
event.content = draft?.price ? encryptedContent : draft.content;
|
||||||
|
event.created_at = Math.floor(Date.now() / 1000);
|
||||||
|
event.pubkey = user.pubkey;
|
||||||
|
event.tags = [
|
||||||
|
['d', dTag],
|
||||||
|
['title', draft.title],
|
||||||
|
['summary', draft.summary],
|
||||||
|
['image', draft.image],
|
||||||
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
|
];
|
||||||
|
|
||||||
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublishedResource = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// create new object with state fields
|
||||||
|
const updatedDraft = {
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
price,
|
||||||
|
content,
|
||||||
|
image: coverImage,
|
||||||
|
topics: [...topics.map(topic => topic.trim().toLowerCase()), 'plebdevs', 'resource']
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('handlePublishedResource', updatedDraft);
|
||||||
|
|
||||||
|
const event = await buildEvent(updatedDraft);
|
||||||
|
|
||||||
|
console.log('event', event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ndk.connect();
|
||||||
|
|
||||||
|
const published = await ndk.publish(event);
|
||||||
|
|
||||||
|
if (published) {
|
||||||
|
showToast('success', 'Success', 'Resource published successfully.');
|
||||||
|
router.push(`/resource/${event.id}`);
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Error', 'Failed to publish resource. Please try again.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
showToast('error', 'Error', 'Failed to publish resource. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -115,7 +186,7 @@ const ResourceForm = ({ draft = null }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={isPublished && draft ? handlePublishedResource : handleSubmit}>
|
||||||
<div className="p-inputgroup flex-1">
|
<div className="p-inputgroup flex-1">
|
||||||
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,7 +89,7 @@ const UserAvatar = () => {
|
|||||||
icon="pi pi-user"
|
icon="pi pi-user"
|
||||||
className="text-[#f8f8ff]"
|
className="text-[#f8f8ff]"
|
||||||
rounded
|
rounded
|
||||||
onClick={() => router.push('/login')}
|
onClick={() => router.push('/auth/signin')}
|
||||||
size={windowWidth < 768 ? 'small' : 'normal'}
|
size={windowWidth < 768 ? 'small' : 'normal'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,19 @@ export const getResourceById = async (id) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function isResourcePartOfAnyCourse(resourceId) {
|
||||||
|
const courses = await prisma.course.findMany({
|
||||||
|
where: {
|
||||||
|
resources: {
|
||||||
|
some: {
|
||||||
|
id: resourceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return courses.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export const createResource = async (data) => {
|
export const createResource = async (data) => {
|
||||||
return await prisma.resource.create({
|
return await prisma.resource.create({
|
||||||
data,
|
data,
|
||||||
|
@ -1,46 +1,71 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
|
import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||||
|
|
||||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY
|
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||||
|
|
||||||
export function useCoursesQuery() {
|
export function useCoursesQuery() {
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||||
const ndk = useNDKContext();
|
const ndk = useNDKContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true);
|
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 fetchCoursesFromNDK = async () => {
|
const fetchCoursesFromNDK = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Fetching courses from NDK');
|
if (contentIdsLoading) {
|
||||||
|
return []; // or a loading state indication
|
||||||
|
}
|
||||||
|
if (contentIdsError) {
|
||||||
|
console.error('Error fetching content IDs:', contentIdsError);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!contentIds) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
await ndk.connect();
|
await ndk.connect();
|
||||||
|
|
||||||
const filter = { kinds: [30004], authors: [AUTHOR_PUBKEY] };
|
const filter = { kinds: [30004], authors: [AUTHOR_PUBKEY] };
|
||||||
const events = await ndk.fetchEvents(filter);
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
|
||||||
if (events && events.size > 0) {
|
if (events && events.size > 0) {
|
||||||
const eventsArray = Array.from(events);
|
const eventsArray = Array.from(events);
|
||||||
console.log('eventsArray', eventsArray)
|
const courses = eventsArray.filter(event => hasRequiredProperties(event));
|
||||||
// const resources = eventsArray.filter(event => hasRequiredTags(event.tags));
|
return courses;
|
||||||
// return resources;
|
|
||||||
return eventsArray;
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching workshops from NDK:', error);
|
console.error('Error fetching courses from NDK:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: courses, isLoading: coursesLoading, error: coursesError, refetch: refetchCourses } = useQuery({
|
const { data: courses, isLoading: coursesLoading, error: coursesError, refetch: refetchCourses } = useQuery({
|
||||||
queryKey: ['courses', isClient],
|
queryKey: ['courses', isClient],
|
||||||
queryFn: fetchCoursesFromNDK,
|
queryFn: fetchCoursesFromNDK,
|
||||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||||
enabled: isClient,
|
enabled: isClient,
|
||||||
})
|
});
|
||||||
|
|
||||||
return { courses, coursesLoading, coursesError, refetchCourses }
|
return { courses, coursesLoading, coursesError, refetchCourses };
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,47 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
|
import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||||
|
|
||||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY
|
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY
|
||||||
|
|
||||||
export function useResourcesQuery() {
|
export function useResourcesQuery() {
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
const ndk = useNDKContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||||
setIsClient(true);
|
const ndk = useNDKContext();
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hasRequiredTags = (tags) => {
|
useEffect(() => {
|
||||||
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
setIsClient(true);
|
||||||
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "resource");
|
}, []);
|
||||||
return hasPlebDevs && hasWorkshop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchResourcesFromNDK = async () => {
|
useEffect(() => {
|
||||||
try {
|
refetchContentIds();
|
||||||
|
}, [refetchContentIds]);
|
||||||
|
|
||||||
|
const hasRequiredProperties = (event) => {
|
||||||
|
if (!contentIds) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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');
|
console.log('Fetching workshops from NDK');
|
||||||
await ndk.connect();
|
await ndk.connect();
|
||||||
|
|
||||||
@ -27,19 +49,19 @@ const fetchResourcesFromNDK = async () => {
|
|||||||
const events = await ndk.fetchEvents(filter);
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
|
||||||
if (events && events.size > 0) {
|
if (events && events.size > 0) {
|
||||||
const eventsArray = Array.from(events);
|
const eventsArray = Array.from(events);
|
||||||
console.log('eventsArray', eventsArray)
|
console.log('eventsArray', eventsArray)
|
||||||
const resources = eventsArray.filter(event => hasRequiredTags(event.tags));
|
const resources = eventsArray.filter(event => hasRequiredProperties(event));
|
||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching workshops from NDK:', error);
|
console.error('Error fetching workshops from NDK:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: resources, isLoading: resourcesLoading, error: resourcesError, refetch: refetchResources } = useQuery({
|
const { data: resources, isLoading: resourcesLoading, error: resourcesError, refetch: refetchResources } = useQuery({
|
||||||
queryKey: ['resources', isClient],
|
queryKey: ['resources', isClient],
|
||||||
queryFn: fetchResourcesFromNDK,
|
queryFn: fetchResourcesFromNDK,
|
||||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||||
|
@ -1,51 +1,72 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
|
import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||||
|
|
||||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY
|
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||||
|
|
||||||
export function useWorkshopsQuery() {
|
export function useWorkshopsQuery() {
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||||
const ndk = useNDKContext();
|
const ndk = useNDKContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true);
|
setIsClient(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hasRequiredTags = (tags) => {
|
useEffect(() => {
|
||||||
const hasPlebDevs = tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
refetchContentIds();
|
||||||
const hasWorkshop = tags.some(([tag, value]) => tag === "t" && value === "workshop");
|
}, [refetchContentIds]);
|
||||||
return hasPlebDevs && hasWorkshop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchWorkshopsFromNDK = async () => {
|
const hasRequiredProperties = (event) => {
|
||||||
try {
|
if (contentIdsLoading) {
|
||||||
console.log('Fetching workshops from NDK');
|
return false;
|
||||||
await ndk.connect();
|
|
||||||
|
|
||||||
const filter = { kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] };
|
|
||||||
const events = await ndk.fetchEvents(filter);
|
|
||||||
|
|
||||||
if (events && events.size > 0) {
|
|
||||||
const eventsArray = Array.from(events);
|
|
||||||
console.log('eventsArray', eventsArray)
|
|
||||||
const resources = eventsArray.filter(event => hasRequiredTags(event.tags));
|
|
||||||
return resources;
|
|
||||||
}
|
}
|
||||||
return [];
|
const hasPlebDevs = event.tags.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||||
} catch (error) {
|
const hasWorkshop = event.tags.some(([tag, value]) => tag === "t" && value === "workshop");
|
||||||
console.error('Error fetching workshops from NDK:', error);
|
const hasId = contentIds.includes(event.id);
|
||||||
return [];
|
return hasPlebDevs && hasWorkshop && hasId;
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const { data: workshops, isLoading: workshopsLoading, error: workshopsError, refetch: refetchWorkshops } = useQuery({
|
const fetchWorkshopsFromNDK = async () => {
|
||||||
queryKey: ['workshops', isClient],
|
try {
|
||||||
queryFn: fetchWorkshopsFromNDK,
|
if (contentIdsLoading) {
|
||||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
return []; // or a loading state indication
|
||||||
refetchInterval: 1000 * 60 * 30, // 30 minutes
|
}
|
||||||
enabled: isClient,
|
if (contentIdsError) {
|
||||||
})
|
console.error('Error fetching content IDs:', contentIdsError);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!contentIds) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
console.log('Fetching workshops from NDK');
|
||||||
|
await ndk.connect();
|
||||||
|
|
||||||
return { workshops, workshopsLoading, workshopsError, refetchWorkshops }
|
const filter = { kinds: [30023, 30402], authors: [AUTHOR_PUBKEY] };
|
||||||
}
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
|
||||||
|
if (events && events.size > 0) {
|
||||||
|
const eventsArray = Array.from(events);
|
||||||
|
console.log('eventsArray', eventsArray);
|
||||||
|
const workshops = eventsArray.filter(event => hasRequiredProperties(event));
|
||||||
|
return workshops;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching workshops from NDK:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
enabled: isClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { workshops, workshopsLoading, workshopsError, refetchWorkshops };
|
||||||
|
}
|
||||||
|
@ -40,11 +40,13 @@ export default NextAuth({
|
|||||||
// Check if user exists, create if not
|
// Check if user exists, create if not
|
||||||
const response = await axios.get(`${BASE_URL}/api/users/${credentials.pubkey}`);
|
const response = await axios.get(`${BASE_URL}/api/users/${credentials.pubkey}`);
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
return response.data;
|
const fields = await findKind0Fields(profile);
|
||||||
|
return { pubkey: credentials.pubkey, ...fields };
|
||||||
} else if (response.status === 204) {
|
} else if (response.status === 204) {
|
||||||
// Create user
|
// Create user
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const fields = await findKind0Fields(profile);
|
const fields = await findKind0Fields(profile);
|
||||||
|
console.log('FEEEEELDS', fields);
|
||||||
const payload = { pubkey: credentials.pubkey, ...fields };
|
const payload = { pubkey: credentials.pubkey, ...fields };
|
||||||
|
|
||||||
const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload);
|
const createUserResponse = await axios.post(`${BASE_URL}/api/users`, payload);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getResourceById, updateResource, deleteResource } from "@/db/models/resourceModels";
|
import { getResourceById, updateResource, deleteResource, isResourcePartOfAnyCourse } from "@/db/models/resourceModels";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { slug } = req.query;
|
const { slug } = req.query;
|
||||||
@ -23,13 +23,17 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
} else if (req.method === 'DELETE') {
|
} else if (req.method === 'DELETE') {
|
||||||
try {
|
try {
|
||||||
await deleteResource(slug);
|
const isPartOfAnyCourse = await isResourcePartOfAnyCourse(slug);
|
||||||
res.status(204).end();
|
if (isPartOfAnyCourse) {
|
||||||
|
res.status(400).json({ error: 'Resource is part of one or more courses' });
|
||||||
|
} else {
|
||||||
|
await deleteResource(slug);
|
||||||
|
res.status(204).end();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle any other HTTP method
|
|
||||||
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
|
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
|
||||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||||
}
|
}
|
||||||
|
47
src/pages/details/[slug]/edit.js
Normal file
47
src/pages/details/[slug]/edit.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { parseEvent } from "@/utils/nostr";
|
||||||
|
import ResourceForm from "@/components/forms/ResourceForm";
|
||||||
|
import WorkshopForm from "@/components/forms/WorkshopForm";
|
||||||
|
import CourseForm from "@/components/forms/CourseForm";
|
||||||
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
||||||
|
export default function Edit() {
|
||||||
|
const [event, setEvent] = useState(null);
|
||||||
|
|
||||||
|
const ndk = useNDKContext();
|
||||||
|
const router = useRouter();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.isReady) {
|
||||||
|
const { slug } = router.query;
|
||||||
|
|
||||||
|
const fetchEvent = async () => {
|
||||||
|
await ndk.connect();
|
||||||
|
|
||||||
|
const fetchedEvent = await ndk.fetchEvent(slug);
|
||||||
|
|
||||||
|
if (fetchedEvent) {
|
||||||
|
const parsedEvent = parseEvent(fetchedEvent);
|
||||||
|
console.log('parsedEvent:', parsedEvent);
|
||||||
|
setEvent(parsedEvent);
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Error', 'Event not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchEvent();
|
||||||
|
}
|
||||||
|
}, [router.isReady, router.query, ndk, showToast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[80vw] max-w-[80vw] mx-auto my-8 flex flex-col justify-center">
|
||||||
|
<h2 className="text-center mb-8">Edit Published Event</h2>
|
||||||
|
{event?.topics.includes('course') && <CourseForm draft={event} isPublished />}
|
||||||
|
{event?.topics.includes('workshop') && <WorkshopForm draft={event} isPublished />}
|
||||||
|
{event?.topics.includes('resource') && <ResourceForm draft={event} isPublished />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,15 +1,18 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { parseEvent, findKind0Fields, hexToNpub } from '@/utils/nostr';
|
import { parseEvent, findKind0Fields } from '@/utils/nostr';
|
||||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||||
import { getSatAmountFromInvoice } from '@/utils/lightning';
|
import { getSatAmountFromInvoice } from '@/utils/lightning';
|
||||||
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
||||||
import { Tag } from 'primereact/tag';
|
import { Tag } from 'primereact/tag';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
import { nip19, nip04 } from 'nostr-tools';
|
import { nip19, nip04 } from 'nostr-tools';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription';
|
import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
@ -38,11 +41,13 @@ export default function Details() {
|
|||||||
const [zapAmount, setZapAmount] = useState(null);
|
const [zapAmount, setZapAmount] = useState(null);
|
||||||
const [paidResource, setPaidResource] = useState(false);
|
const [paidResource, setPaidResource] = useState(false);
|
||||||
const [decryptedContent, setDecryptedContent] = useState(null);
|
const [decryptedContent, setDecryptedContent] = useState(null);
|
||||||
|
const [authorView, setAuthorView] = useState(false);
|
||||||
|
|
||||||
const ndk = useNDKContext();
|
const ndk = useNDKContext();
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
const { showToast } = useToast();
|
||||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
|
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -102,6 +107,9 @@ export default function Details() {
|
|||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
|
if (user && user.pubkey === event.pubkey) {
|
||||||
|
setAuthorView(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching event:', error);
|
console.error('Error fetching event:', error);
|
||||||
@ -111,7 +119,7 @@ export default function Details() {
|
|||||||
fetchEvent(slug);
|
fetchEvent(slug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [router.isReady, router.query, ndk]);
|
}, [router.isReady, router.query, ndk, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAuthor = async (pubkey) => {
|
const fetchAuthor = async (pubkey) => {
|
||||||
@ -171,6 +179,25 @@ export default function Details() {
|
|||||||
setZapAmount(total);
|
setZapAmount(total);
|
||||||
}, [zaps]);
|
}, [zaps]);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/resources/${processedEvent.id}`);
|
||||||
|
if (response.status === 204) {
|
||||||
|
showToast('success', 'Success', 'Resource deleted successfully.');
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response && error.response.data && error.response.data.error.includes("Invalid `prisma.resource.delete()`")) {
|
||||||
|
showToast('error', 'Error', 'Resource cannot be deleted because it is part of a course, delete the course first.');
|
||||||
|
}
|
||||||
|
else if (error.response && error.response.data && error.response.data.error) {
|
||||||
|
showToast('error', 'Error', error.response.data.error);
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Error', 'Failed to delete resource. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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 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-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col'>
|
||||||
@ -227,6 +254,14 @@ export default function Details() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{authorView && (
|
||||||
|
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
|
||||||
|
<div className='w-fit flex flex-row justify-between'>
|
||||||
|
<Button onClick={() => router.push(`/details/${processedEvent.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>
|
||||||
|
)}
|
||||||
{typeof window !== 'undefined' && nAddress !== null && (
|
{typeof window !== 'undefined' && nAddress !== null && (
|
||||||
<div className='px-24'>
|
<div className='px-24'>
|
||||||
<ZapThreadsWrapper
|
<ZapThreadsWrapper
|
@ -1,105 +1,105 @@
|
|||||||
import React, { useRef, useState, useEffect, use } from "react";
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { Button } from "primereact/button";
|
import { Button } from "primereact/button";
|
||||||
import { DataTable } from "primereact/datatable";
|
import { DataTable } from "primereact/datatable";
|
||||||
import { Menu } from "primereact/menu";
|
import { Menu } from "primereact/menu";
|
||||||
import { Column } from "primereact/column";
|
import { Column } from "primereact/column";
|
||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import UserContent from "@/components/profile/UserContent";
|
import UserContent from "@/components/profile/UserContent";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import BitcoinConnectButton from "@/components/profile/BitcoinConnect";
|
import BitcoinConnectButton from "@/components/profile/BitcoinConnect";
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { data: session, status } = useSession();
|
const [user, setUser] = useState(null);
|
||||||
const [user, setUser] = useState(null);
|
|
||||||
const { returnImageProxy } = useImageProxy();
|
|
||||||
const menu = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: session, status } = useSession();
|
||||||
if (session) {
|
const { returnImageProxy } = useImageProxy();
|
||||||
setUser(session.user);
|
const menu = useRef(null);
|
||||||
}
|
|
||||||
}, [session]);
|
|
||||||
|
|
||||||
const purchases = [];
|
useEffect(() => {
|
||||||
|
if (session) {
|
||||||
|
setUser(session.user);
|
||||||
|
}
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
const menuItems = [
|
const purchases = [];
|
||||||
{
|
|
||||||
label: "Edit",
|
|
||||||
icon: "pi pi-pencil",
|
|
||||||
command: () => {
|
|
||||||
// Add your edit functionality here
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Delete",
|
|
||||||
icon: "pi pi-trash",
|
|
||||||
command: () => {
|
|
||||||
// Add your delete functionality here
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const header = (
|
const menuItems = [
|
||||||
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
|
{
|
||||||
<span className="text-xl text-900 font-bold text-white">Purchases</span>
|
label: "Edit",
|
||||||
</div>
|
icon: "pi pi-pencil",
|
||||||
);
|
command: () => {
|
||||||
|
// Add your edit functionality here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
icon: "pi pi-trash",
|
||||||
|
command: () => {
|
||||||
|
// Add your delete functionality here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
const header = (
|
||||||
user && (
|
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
<span className="text-xl text-900 font-bold text-white">Purchases</span>
|
||||||
<div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
|
||||||
<div className="relative flex w-full items-center justify-center">
|
|
||||||
<Image
|
|
||||||
alt="user's avatar"
|
|
||||||
src={returnImageProxy(user.avatar, user.pubkey)}
|
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
className="rounded-full my-4"
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75"
|
|
||||||
onClick={(e) => menu.current.toggle(e)}
|
|
||||||
></i>
|
|
||||||
<Menu model={menuItems} popup ref={menu} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-center text-2xl my-2">
|
|
||||||
{user.username || "Anon"}
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
|
|
||||||
{user.pubkey}
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
|
||||||
<h2>Connect Your Lightning Wallet</h2>
|
|
||||||
<BitcoinConnectButton />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
|
||||||
<h2>Subscription</h2>
|
|
||||||
<p className="text-center">You currently have no active subscription</p>
|
|
||||||
<Button
|
|
||||||
label="Subscribe"
|
|
||||||
className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
);
|
||||||
emptyMessage="No purchases"
|
|
||||||
value={purchases}
|
return (
|
||||||
tableStyle={{ minWidth: "100%" }}
|
user && (
|
||||||
header={header}
|
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
||||||
>
|
<div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
|
||||||
<Column field="cost" header="Cost"></Column>
|
<div className="relative flex w-full items-center justify-center">
|
||||||
<Column field="name" header="Name"></Column>
|
<Image
|
||||||
<Column field="category" header="Category"></Column>
|
alt="user's avatar"
|
||||||
<Column field="date" header="Date"></Column>
|
src={returnImageProxy(user.avatar, user.pubkey)}
|
||||||
</DataTable>
|
width={100}
|
||||||
<UserContent />
|
height={100}
|
||||||
</div>
|
className="rounded-full my-4"
|
||||||
)
|
/>
|
||||||
);
|
<i
|
||||||
|
className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75"
|
||||||
|
onClick={(e) => menu.current.toggle(e)}
|
||||||
|
></i>
|
||||||
|
<Menu model={menuItems} popup ref={menu} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-center text-2xl my-2">
|
||||||
|
{user.username || "Anon"}
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
|
||||||
|
{user.pubkey}
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||||
|
<h2>Connect Your Lightning Wallet</h2>
|
||||||
|
<BitcoinConnectButton />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||||
|
<h2>Subscription</h2>
|
||||||
|
<p className="text-center">You currently have no active subscription</p>
|
||||||
|
<Button
|
||||||
|
label="Subscribe"
|
||||||
|
className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
emptyMessage="No purchases"
|
||||||
|
value={purchases}
|
||||||
|
tableStyle={{ minWidth: "100%" }}
|
||||||
|
header={header}
|
||||||
|
>
|
||||||
|
<Column field="cost" header="Cost"></Column>
|
||||||
|
<Column field="name" header="Name"></Column>
|
||||||
|
<Column field="category" header="Category"></Column>
|
||||||
|
<Column field="date" header="Date"></Column>
|
||||||
|
</DataTable>
|
||||||
|
<UserContent />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
export const findKind0Fields = async (kind0) => {
|
export const findKind0Fields = async (kind0) => {
|
||||||
|
console.log('kind0', kind0);
|
||||||
let fields = {}
|
let fields = {}
|
||||||
|
|
||||||
const usernameProperties = ['name', 'displayName', 'display_name', 'username', 'handle', 'alias'];
|
const usernameProperties = ['name', 'displayName', 'display_name', 'username', 'handle', 'alias'];
|
||||||
@ -20,7 +21,7 @@ export const findKind0Fields = async (kind0) => {
|
|||||||
fields.username = username;
|
fields.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatar = findTruthyPropertyValue(kind0, ['picture', 'avatar', 'profilePicture', 'profile_picture']);
|
const avatar = findTruthyPropertyValue(kind0, ['picture', 'avatar', 'profilePicture', 'profile_picture', 'image']);
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
fields.avatar = avatar;
|
fields.avatar = avatar;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user