Segregate paid resources with paid courses and vice versa, fix paid field on workshop

This commit is contained in:
austinkelsay 2024-08-17 12:56:27 -05:00
parent cee1679a8b
commit 374bef5a51
7 changed files with 105 additions and 59 deletions

View File

@ -31,7 +31,9 @@ export default function WorkshopsCarousel() {
const fetch = async () => { const fetch = async () => {
try { try {
if (workshops && workshops.length > 0) { if (workshops && workshops.length > 0) {
console.log('workshops', workshops);
const processedWorkshops = workshops.map(workshop => parseEvent(workshop)); const processedWorkshops = workshops.map(workshop => parseEvent(workshop));
console.log('processedWorkshops', processedWorkshops);
setProcessedWorkshops(processedWorkshops); setProcessedWorkshops(processedWorkshops);
} else { } else {
console.log('No workshops fetched or empty array returned'); console.log('No workshops fetched or empty array returned');

View File

@ -13,7 +13,8 @@ import { useNDKContext } from "@/context/NDKContext";
import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription'; import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription';
import { findKind0Fields } from '@/utils/nostr'; import { findKind0Fields } from '@/utils/nostr';
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
import ResourcePaymentButton from "@/components/bitcoinConnect/ResourcePaymentButton"; import CoursePaymentButton from "@/components/bitcoinConnect/CoursePaymentButton";
import { ProgressSpinner } from 'primereact/progressspinner';
const MDDisplay = dynamic( const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"), () => 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 [author, setAuthor] = useState(null);
const [nAddress, setNAddress] = useState(null); const [nAddress, setNAddress] = useState(null);
const [zapAmount, setZapAmount] = useState(0); const [zapAmount, setZapAmount] = useState(0);
@ -45,6 +46,10 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
console.log("paidCourse", paidCourse); console.log("paidCourse", paidCourse);
}, [paidCourse]); }, [paidCourse]);
useEffect(() => {
console.log("decryptionPerformed", decryptionPerformed);
}, [decryptionPerformed]);
useEffect(() => { useEffect(() => {
if (session) { if (session) {
setUser(session.user); setUser(session.user);
@ -86,6 +91,14 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
setZapAmount(total); setZapAmount(total);
}, [zaps, processedEvent]); }, [zaps, processedEvent]);
if (!processedEvent || !author) {
return (
<div className="flex justify-center items-center h-screen">
<ProgressSpinner />
</div>
);
}
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'>
@ -128,8 +141,8 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
className="w-[344px] h-[194px] object-cover object-top rounded-lg" className="w-[344px] h-[194px] object-cover object-top rounded-lg"
/> />
<div className='w-full flex justify-between items-center'> <div className='w-full flex justify-between items-center'>
{paidCourse && !decryptedContent && ( {paidCourse && !decryptionPerformed && (
<ResourcePaymentButton <CoursePaymentButton
lnAddress={'bitcoinplebdev@stacker.news'} lnAddress={'bitcoinplebdev@stacker.news'}
amount={processedEvent.price} amount={processedEvent.price}
onSuccess={handlePaymentSuccess} onSuccess={handlePaymentSuccess}
@ -137,7 +150,7 @@ export default function CourseDetails({ processedEvent, paidCourse, decryptedCon
resourceId={processedEvent.d} 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> <p className='text-green-500'>Paid {processedEvent.price} sats</p>
)} )}
{paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey && ( {paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey && (

View File

@ -10,14 +10,6 @@ const ContentListItem = (content) => {
const isDraft = Object.keys(content).includes('type'); const isDraft = Object.keys(content).includes('type');
const isCourse = content && content?.kind === 30004; 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 = () => { const handleClick = () => {
let path = ''; let path = '';

View File

@ -26,8 +26,8 @@ import 'primeicons/primeicons.css';
const CourseForm = ({ draft = null, isPublished = false }) => { const CourseForm = ({ draft = null, isPublished = false }) => {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [summary, setSummary] = useState(''); const [summary, setSummary] = useState('');
const [checked, setChecked] = useState(false); const [isPaidCourse, setIsPaidCourse] = useState(draft?.price ? true : false);
const [price, setPrice] = useState(0); const [price, setPrice] = useState(draft?.price || 0);
const [coverImage, setCoverImage] = useState(''); const [coverImage, setCoverImage] = useState('');
const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]); const [lessons, setLessons] = useState([{ id: uuidv4(), title: 'Select a lesson' }]);
const [loadingLessons, setLoadingLessons] = useState(true); const [loadingLessons, setLoadingLessons] = useState(true);
@ -89,7 +89,7 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
console.log('draft:', draft); console.log('draft:', draft);
setTitle(draft.title); setTitle(draft.title);
setSummary(draft.summary); setSummary(draft.summary);
setChecked(draft.price > 0); setIsPaidCourse(draft.price > 0);
setPrice(draft.price || 0); setPrice(draft.price || 0);
setCoverImage(draft.image); setCoverImage(draft.image);
// setSelectedLessons(draft.resources || []); // setSelectedLessons(draft.resources || []);
@ -97,6 +97,10 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
} }
}, [draft]); }, [draft]);
const handlePriceChange = (value) => {
setPrice(value);
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -198,12 +202,20 @@ const CourseForm = ({ draft = null, isPublished = false }) => {
if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) { if (resourcesLoading || !resources || workshopsLoading || !workshops || draftsLoading || !drafts) {
return []; 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} />, label: <ContentDropdownItem content={draft} onSelect={(content) => handleLessonSelect(content, index)} selected={lessons[index] && lessons[index].id === draft.id} />,
value: 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); const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(resource);
return { 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} />, 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); const { id, kind, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(workshop);
return { 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} />, 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"> <div className="p-inputgroup flex-1 mt-4">
<InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" /> <InputText value={coverImage} onChange={(e) => setCoverImage(e.target.value)} placeholder="Cover Image URL" />
</div> </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> <p className="py-2">Paid Course</p>
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} /> <InputSwitch checked={isPaidCourse} onChange={(e) => setIsPaidCourse(e.value)} />
{checked && ( {isPaidCourse && (
<div className="p-inputgroup flex-1 py-4"> <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>
)} )}
</div> </div>

View File

@ -25,6 +25,7 @@ const EditCourseForm = ({ draft }) => {
const [summary, setSummary] = useState(''); const [summary, setSummary] = useState('');
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
const [isPaid, setIsPaid] = useState(false);
const [coverImage, setCoverImage] = useState(''); const [coverImage, setCoverImage] = useState('');
const [selectedLessons, setSelectedLessons] = useState([]); const [selectedLessons, setSelectedLessons] = useState([]);
const [selectedLessonsLoading, setSelectedLessonsLoading] = useState(false); const [selectedLessonsLoading, setSelectedLessonsLoading] = useState(false);
@ -63,11 +64,17 @@ const EditCourseForm = ({ draft }) => {
setSummary(draft.summary); setSummary(draft.summary);
setChecked(draft.price > 0); setChecked(draft.price > 0);
setPrice(draft.price || 0); setPrice(draft.price || 0);
setIsPaid(draft.price > 0);
setCoverImage(draft.image); setCoverImage(draft.image);
setTopics(draft.topics || ['']); setTopics(draft.topics || ['']);
} }
}, [draft, ndk, showToast, parseEvent]); }, [draft, ndk, showToast, parseEvent]);
const handlePriceChange = (value) => {
setPrice(value);
setIsPaid(value > 0);
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -90,7 +97,7 @@ const EditCourseForm = ({ draft }) => {
title, title,
summary, summary,
image: coverImage, image: coverImage,
price: checked ? price : 0, price: isPaid ? price : 0,
topics, topics,
resourceIds: lessonsToUpdate.filter(lesson => lesson && lesson.id).map(lesson => lesson.id) resourceIds: lessonsToUpdate.filter(lesson => lesson && lesson.id).map(lesson => lesson.id)
}; };
@ -137,7 +144,12 @@ const EditCourseForm = ({ draft }) => {
return []; 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); const parsedResource = parseEvent(resource);
return { return {
label: <ContentDropdownItem content={parsedResource} onSelect={handleLessonSelect} selected={selectedLessons.some(lesson => lesson.id === parsedResource.id)} />, 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); const parsedWorkshop = parseEvent(workshop);
return { return {
label: <ContentDropdownItem content={parsedWorkshop} onSelect={handleLessonSelect} selected={selectedLessons.some(lesson => lesson.id === parsedWorkshop.id)} />, 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)} /> <InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} />
{checked && ( {checked && (
<div className="p-inputgroup flex-1 py-4"> <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>
)} )}
</div> </div>

View File

@ -8,6 +8,7 @@ import { useNDKContext } from "@/context/NDKContext";
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { nip04 } from 'nostr-tools'; import { nip04 } from 'nostr-tools';
import { ProgressSpinner } from 'primereact/progressspinner';
const MDDisplay = dynamic( const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"), () => import("@uiw/react-markdown-preview"),
@ -21,12 +22,13 @@ const Course = () => {
const [lessonIds, setLessonIds] = useState([]); const [lessonIds, setLessonIds] = useState([]);
const [lessons, setLessons] = useState([]); const [lessons, setLessons] = useState([]);
const [paidCourse, setPaidCourse] = useState(false); const [paidCourse, setPaidCourse] = useState(false);
const [decryptedContent, setDecryptedContent] = useState(null); const [decryptionPerformed, setDecryptionPerformed] = useState(false);
const [loading, setLoading] = useState(true);
const router = useRouter(); const router = useRouter();
const {ndk, addSigner} = useNDKContext(); const {ndk, addSigner} = useNDKContext();
const { data: session, update } = useSession(); const { data: session, update } = useSession();
const { showToast } = useToast(); const { showToast } = useToast();
const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY; const privkey = process.env.NEXT_PUBLIC_APP_PRIV_KEY;
const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY; const pubkey = process.env.NEXT_PUBLIC_APP_PUBLIC_KEY;
@ -112,21 +114,37 @@ const Course = () => {
useEffect(() => { useEffect(() => {
const decryptContent = async () => { const decryptContent = async () => {
if (session?.user && paidCourse) { if (session?.user && paidCourse && !decryptionPerformed) {
if (session.user?.purchased?.length > 0) { setLoading(true);
const purchasedCourse = session.user.purchased.find(purchase => purchase.resourceId === course.d); const canAccess =
if (purchasedCourse) { session.user.purchased?.some(purchase => purchase.courseId === course?.d) ||
const decryptedContent = await nip04.decrypt(privkey, pubkey, course.content); session.user?.role?.subscribed ||
setDecryptedContent(decryptedContent); 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(); 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) => { const handlePaymentSuccess = async (response, newCourse) => {
if (response && response?.preimage) { if (response && response?.preimage) {
@ -142,12 +160,21 @@ const Course = () => {
showToast('error', 'Payment Error', `Failed to purchase course. Please try again. Error: ${error}`); 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 ( return (
<> <>
<CourseDetails <CourseDetails
processedEvent={course} processedEvent={course}
paidCourse={paidCourse} paidCourse={paidCourse}
decryptedContent={decryptedContent} lessons={lessons}
decryptionPerformed={decryptionPerformed}
handlePaymentSuccess={handlePaymentSuccess} handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError} handlePaymentError={handlePaymentError}
/> />
@ -155,9 +182,7 @@ const Course = () => {
<CourseLesson key={index} lesson={lesson} course={course} /> <CourseLesson key={index} lesson={lesson} course={course} />
))} ))}
<div className="mx-auto my-6"> <div className="mx-auto my-6">
{ {course?.content && <MDDisplay source={course.content} />}
course?.content && <MDDisplay source={course.content} />
}
</div> </div>
</> </>
); );

View File

@ -227,26 +227,11 @@ export default function Draft() {
['image', draft.image], ['image', draft.image],
...draft.topics.map(topic => ['t', topic]), ...draft.topics.map(topic => ['t', topic]),
['published_at', Math.floor(Date.now() / 1000).toString()], ['published_at', Math.floor(Date.now() / 1000).toString()],
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
]; ];
type = 'workshop'; type = 'workshop';
break; 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: default:
return null; return null;
} }