mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Segregate paid resources with paid courses and vice versa, fix paid field on workshop
This commit is contained in:
parent
cee1679a8b
commit
374bef5a51
@ -31,7 +31,9 @@ export default function WorkshopsCarousel() {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
if (workshops && workshops.length > 0) {
|
||||
console.log('workshops', workshops);
|
||||
const processedWorkshops = workshops.map(workshop => parseEvent(workshop));
|
||||
console.log('processedWorkshops', processedWorkshops);
|
||||
setProcessedWorkshops(processedWorkshops);
|
||||
} else {
|
||||
console.log('No workshops fetched or empty array returned');
|
||||
|
@ -13,7 +13,8 @@ import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription';
|
||||
import { findKind0Fields } from '@/utils/nostr';
|
||||
import 'primeicons/primeicons.css';
|
||||
import ResourcePaymentButton from "@/components/bitcoinConnect/ResourcePaymentButton";
|
||||
import CoursePaymentButton from "@/components/bitcoinConnect/CoursePaymentButton";
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
@ -22,7 +23,7 @@ const MDDisplay = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
export default function CourseDetails({ processedEvent, paidCourse, decryptedContent, handlePaymentSuccess, handlePaymentError }) {
|
||||
export default function CourseDetails({ processedEvent, paidCourse, lessons, decryptionPerformed, handlePaymentSuccess, handlePaymentError }) {
|
||||
const [author, setAuthor] = useState(null);
|
||||
const [nAddress, setNAddress] = useState(null);
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
@ -45,6 +46,10 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
|
||||
console.log("paidCourse", paidCourse);
|
||||
}, [paidCourse]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("decryptionPerformed", decryptionPerformed);
|
||||
}, [decryptionPerformed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
setUser(session.user);
|
||||
@ -86,6 +91,14 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
|
||||
setZapAmount(total);
|
||||
}, [zaps, processedEvent]);
|
||||
|
||||
if (!processedEvent || !author) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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'>
|
||||
@ -128,8 +141,8 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
|
||||
className="w-[344px] h-[194px] object-cover object-top rounded-lg"
|
||||
/>
|
||||
<div className='w-full flex justify-between items-center'>
|
||||
{paidCourse && !decryptedContent && (
|
||||
<ResourcePaymentButton
|
||||
{paidCourse && !decryptionPerformed && (
|
||||
<CoursePaymentButton
|
||||
lnAddress={'bitcoinplebdev@stacker.news'}
|
||||
amount={processedEvent.price}
|
||||
onSuccess={handlePaymentSuccess}
|
||||
@ -137,7 +150,7 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
|
||||
resourceId={processedEvent.d}
|
||||
/>
|
||||
)}
|
||||
{paidCourse && decryptedContent && author && processedEvent?.pubkey !== session?.user?.pubkey && (
|
||||
{paidCourse && decryptionPerformed && author && processedEvent?.pubkey !== session?.user?.pubkey && (
|
||||
<p className='text-green-500'>Paid {processedEvent.price} sats</p>
|
||||
)}
|
||||
{paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey && (
|
||||
|
@ -10,14 +10,6 @@ const ContentListItem = (content) => {
|
||||
const isDraft = Object.keys(content).includes('type');
|
||||
const isCourse = content && content?.kind === 30004;
|
||||
|
||||
useEffect(() => {
|
||||
if (content && content?.kind === 30004) {
|
||||
console.log("isDraft", isDraft);
|
||||
console.log("content", content);
|
||||
console.log("isCourse", isCourse);
|
||||
}
|
||||
}, [content, isDraft, isCourse]);
|
||||
|
||||
const handleClick = () => {
|
||||
let path = '';
|
||||
|
||||
|
@ -26,8 +26,8 @@ import 'primeicons/primeicons.css';
|
||||
const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [summary, setSummary] = useState('');
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [price, setPrice] = useState(0);
|
||||
const [isPaidCourse, setIsPaidCourse] = useState(draft?.price ? true : false);
|
||||
const [price, setPrice] = useState(draft?.price || 0);
|
||||
const [coverImage, setCoverImage] = useState('');
|
||||
const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]);
|
||||
const [loadingLessons, setLoadingLessons] = useState(true);
|
||||
@ -89,7 +89,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
console.log('draft:', draft);
|
||||
setTitle(draft.title);
|
||||
setSummary(draft.summary);
|
||||
setChecked(draft.price > 0);
|
||||
setIsPaidCourse(draft.price > 0);
|
||||
setPrice(draft.price || 0);
|
||||
setCoverImage(draft.image);
|
||||
// setSelectedLessons(draft.resources || []);
|
||||
@ -97,6 +97,10 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
}
|
||||
}, [draft]);
|
||||
|
||||
const handlePriceChange = (value) => {
|
||||
setPrice(value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -198,12 +202,20 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) {
|
||||
return [];
|
||||
}
|
||||
const draftOptions = drafts.map(draft => ({
|
||||
|
||||
const filterContent = (content) => {
|
||||
console.log('contentttttt', content);
|
||||
// If there is price in content.tags, then it is a paid content 'price' in the 0 index and stringified int in the 1 index
|
||||
const contentPrice = content.tags.find(tag => tag[0] === 'price') ? parseInt(content.tags.find(tag => tag[0] === 'price')[1]) : 0;
|
||||
return isPaidCourse ? contentPrice > 0 : contentPrice === 0;
|
||||
};
|
||||
|
||||
const draftOptions = drafts.filter(filterContent).map(draft => ({
|
||||
label: <ContentDropdownItem content={draft} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />,
|
||||
value: draft.id
|
||||
}));
|
||||
|
||||
const resourceOptions = resources.map(resource => {
|
||||
const resourceOptions = resources.filter(filterContent).map(resource => {
|
||||
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resource);
|
||||
return {
|
||||
label: <ContentDropdownItem content={{ id, kind, pubkey, content, title, summary, image, published_at, d, topics }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
|
||||
@ -211,7 +223,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
};
|
||||
});
|
||||
|
||||
const workshopOptions = workshops.map(workshop => {
|
||||
const workshopOptions = workshops.filter(filterContent).map(workshop => {
|
||||
const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(workshop);
|
||||
return {
|
||||
label: <ContentDropdownItem content={{ id, kind, pubkey, content, title, summary, image, published_at, d, topics }} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === id} />,
|
||||
@ -251,12 +263,17 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
|
||||
</div>
|
||||
<div className="p-inputgroup flex-1 mt-4 flex-col">
|
||||
<div className="p-inputgroup flex-1 mt-8 flex-col">
|
||||
<p className="py-2">Paid Course</p>
|
||||
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} />
|
||||
{checked && (
|
||||
<InputSwitch checked={isPaidCourse} onChange={(e) => setIsPaidCourse(e.value)} />
|
||||
{isPaidCourse && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
|
||||
<InputNumber
|
||||
value={price}
|
||||
onValueChange={(e) => handlePriceChange(e.value)}
|
||||
placeholder="Price (sats)"
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -25,6 +25,7 @@ const EditCourseForm = ({ draft }) => {
|
||||
const [summary, setSummary] = useState('');
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [price, setPrice] = useState(0);
|
||||
const [isPaid, setIsPaid] = useState(false);
|
||||
const [coverImage, setCoverImage] = useState('');
|
||||
const [selectedLessons, setSelectedLessons] = useState([]);
|
||||
const [selectedLessonsLoading, setSelectedLessonsLoading] = useState(false);
|
||||
@ -63,11 +64,17 @@ const EditCourseForm = ({ draft }) => {
|
||||
setSummary(draft.summary);
|
||||
setChecked(draft.price > 0);
|
||||
setPrice(draft.price || 0);
|
||||
setIsPaid(draft.price > 0);
|
||||
setCoverImage(draft.image);
|
||||
setTopics(draft.topics || ['']);
|
||||
}
|
||||
}, [draft, ndk, showToast, parseEvent]);
|
||||
|
||||
const handlePriceChange = (value) => {
|
||||
setPrice(value);
|
||||
setIsPaid(value > 0);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -90,7 +97,7 @@ const EditCourseForm = ({ draft }) => {
|
||||
title,
|
||||
summary,
|
||||
image: coverImage,
|
||||
price: checked ? price : 0,
|
||||
price: isPaid ? price : 0,
|
||||
topics,
|
||||
resourceIds: lessonsToUpdate.filter(lesson => lesson && lesson.id).map(lesson => lesson.id)
|
||||
};
|
||||
@ -137,7 +144,12 @@ const EditCourseForm = ({ draft }) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resourceOptions = resources.map(resource => {
|
||||
const filterContent = (content) => {
|
||||
const contentPrice = content.price || 0;
|
||||
return isPaid ? contentPrice > 0 : contentPrice === 0;
|
||||
};
|
||||
|
||||
const resourceOptions = resources.filter(filterContent).map(resource => {
|
||||
const parsedResource = parseEvent(resource);
|
||||
return {
|
||||
label: <ContentDropdownItem content={parsedResource} onSelect={handleLessonSelect} selected={selectedLessons.some(lesson => lesson.id === parsedResource.id)} />,
|
||||
@ -145,7 +157,7 @@ const EditCourseForm = ({ draft }) => {
|
||||
};
|
||||
});
|
||||
|
||||
const workshopOptions = workshops.map(workshop => {
|
||||
const workshopOptions = workshops.filter(filterContent).map(workshop => {
|
||||
const parsedWorkshop = parseEvent(workshop);
|
||||
return {
|
||||
label: <ContentDropdownItem content={parsedWorkshop} onSelect={handleLessonSelect} selected={selectedLessons.some(lesson => lesson.id === parsedWorkshop.id)} />,
|
||||
@ -179,7 +191,7 @@ const EditCourseForm = ({ draft }) => {
|
||||
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} />
|
||||
{checked && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
|
||||
<InputNumber value={price} onValueChange={(e) => handlePriceChange(e.value)} placeholder="Price (sats)" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@ import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { nip04 } from 'nostr-tools';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
@ -21,12 +22,13 @@ const Course = () => {
|
||||
const [lessonIds, setLessonIds] = useState([]);
|
||||
const [lessons, setLessons] = useState([]);
|
||||
const [paidCourse, setPaidCourse] = useState(false);
|
||||
const [decryptedContent, setDecryptedContent] = useState(null);
|
||||
|
||||
const [decryptionPerformed, setDecryptionPerformed] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
const {ndk, addSigner} = useNDKContext();
|
||||
const { data: session, update } = useSession();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY;
|
||||
const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY;
|
||||
|
||||
@ -112,21 +114,37 @@ const Course = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const decryptContent = async () => {
|
||||
if (session?.user && paidCourse) {
|
||||
if (session.user?.purchased?.length > 0) {
|
||||
const purchasedCourse = session.user.purchased.find(purchase => purchase.resourceId === course.d);
|
||||
if (purchasedCourse) {
|
||||
const decryptedContent = await nip04.decrypt(privkey, pubkey, course.content);
|
||||
setDecryptedContent(decryptedContent);
|
||||
if (session?.user && paidCourse && !decryptionPerformed) {
|
||||
setLoading(true);
|
||||
const canAccess =
|
||||
session.user.purchased?.some(purchase => purchase.courseId === course?.d) ||
|
||||
session.user?.role?.subscribed ||
|
||||
session.user?.pubkey === course?.pubkey;
|
||||
|
||||
if (canAccess && lessons.length > 0) {
|
||||
try {
|
||||
const decryptedLessons = await Promise.all(lessons.map(async (lesson) => {
|
||||
const decryptedContent = await nip04.decrypt(privkey, pubkey, lesson.content);
|
||||
return { ...lesson, content: decryptedContent };
|
||||
}));
|
||||
setLessons(decryptedLessons);
|
||||
setDecryptionPerformed(true);
|
||||
} catch (error) {
|
||||
console.error('Error decrypting lessons:', error);
|
||||
}
|
||||
} else if (session.user?.role && session.user.role.subscribed) {
|
||||
const decryptedContent = await nip04.decrypt(privkey, pubkey, course.content);
|
||||
setDecryptedContent(decryptedContent);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
decryptContent();
|
||||
}, [session, paidCourse, course]);
|
||||
}, [session, paidCourse, course, lessons, privkey, pubkey, decryptionPerformed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course && lessons.length > 0 && (!paidCourse || decryptionPerformed)) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [course, lessons, paidCourse, decryptionPerformed]);
|
||||
|
||||
const handlePaymentSuccess = async (response, newCourse) => {
|
||||
if (response && response?.preimage) {
|
||||
@ -142,12 +160,21 @@ const Course = () => {
|
||||
showToast('error', 'Payment Error', `Failed to purchase course. Please try again. Error: ${error}`);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CourseDetails
|
||||
processedEvent={course}
|
||||
paidCourse={paidCourse}
|
||||
decryptedContent={decryptedContent}
|
||||
lessons={lessons}
|
||||
decryptionPerformed={decryptionPerformed}
|
||||
handlePaymentSuccess={handlePaymentSuccess}
|
||||
handlePaymentError={handlePaymentError}
|
||||
/>
|
||||
@ -155,9 +182,7 @@ const Course = () => {
|
||||
<CourseLesson key={index} lesson={lesson} course={course} />
|
||||
))}
|
||||
<div className="mx-auto my-6">
|
||||
{
|
||||
course?.content && <MDDisplay source={course.content} />
|
||||
}
|
||||
{course?.content && <MDDisplay source={course.content} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -227,26 +227,11 @@ export default function Draft() {
|
||||
['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}`]] : []),
|
||||
];
|
||||
|
||||
type = 'workshop';
|
||||
break;
|
||||
case 'course':
|
||||
event.kind = 30023;
|
||||
event.content = draft.content;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = user.pubkey;
|
||||
event.tags = [
|
||||
['d', NewDTag],
|
||||
['title', draft.title],
|
||||
['summary', draft.summary],
|
||||
['image', draft.image],
|
||||
...draft.topics.map(topic => ['t', topic]),
|
||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||
];
|
||||
|
||||
type = 'course';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user