mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
Merge pull request #2 from AustinKelsay/feature/dev-journey-checkboxes
Feature/dev journey checkboxes
This commit is contained in:
commit
46dc942fad
@ -59,14 +59,13 @@ export default function DocumentsCarousel() {
|
||||
// Sort documents by created_at in descending order (most recent first)
|
||||
const sortedDocuments = processedDocuments.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
if (paidLessons && paidLessons.length > 0) {
|
||||
// filter out documents that are in the paid lessons array
|
||||
const filteredDocuments = sortedDocuments.filter(document => !paidLessons.includes(document?.d));
|
||||
// Filter out documents that are in paid lessons and combined resources
|
||||
const filteredDocuments = sortedDocuments.filter(document =>
|
||||
!paidLessons.includes(document?.d) &&
|
||||
!(document.topics?.includes('video') && document.topics?.includes('document'))
|
||||
);
|
||||
|
||||
setProcessedDocuments(filteredDocuments);
|
||||
} else {
|
||||
setProcessedDocuments(sortedDocuments);
|
||||
}
|
||||
setProcessedDocuments(filteredDocuments);
|
||||
} else {
|
||||
console.log('No documents fetched or empty array returned');
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateS
|
||||
import { VideoTemplate } from '@/components/content/carousels/templates/VideoTemplate';
|
||||
import { DocumentTemplate } from '@/components/content/carousels/templates/DocumentTemplate';
|
||||
import { CourseTemplate } from '@/components/content/carousels/templates/CourseTemplate';
|
||||
import { CombinedTemplate } from '@/components/content/carousels/templates/CombinedTemplate';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const responsiveOptions = [
|
||||
@ -74,7 +75,9 @@ export default function GenericCarousel({items, selectedTopic, title}) {
|
||||
value={carouselItems}
|
||||
itemTemplate={(item) => {
|
||||
if (carouselItems.length > 0) {
|
||||
if (item.type === 'document') {
|
||||
if (item.topics?.includes('video') && item.topics?.includes('document')) {
|
||||
return <CombinedTemplate key={item.id} resource={item} isLesson={lessons.includes(item?.d)} />;
|
||||
} else if (item.type === 'document') {
|
||||
return <DocumentTemplate key={item.id} document={item} isLesson={lessons.includes(item?.d)} />;
|
||||
} else if (item.type === 'video') {
|
||||
return <VideoTemplate key={item.id} video={item} isLesson={lessons.includes(item?.d)} />;
|
||||
|
@ -62,14 +62,13 @@ export default function VideosCarousel() {
|
||||
|
||||
const sortedVideos = processedVideos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
if (paidLessons && paidLessons.length > 0) {
|
||||
// filter out videos that are in the paid lessons array
|
||||
const filteredVideos = sortedVideos.filter(video => !paidLessons.includes(video?.d));
|
||||
// Filter out videos that are in paid lessons and combined resources
|
||||
const filteredVideos = sortedVideos.filter(video =>
|
||||
!paidLessons.includes(video?.d) &&
|
||||
!(video.topics?.includes('video') && video.topics?.includes('document'))
|
||||
);
|
||||
|
||||
setProcessedVideos(filteredVideos);
|
||||
} else {
|
||||
setProcessedVideos(sortedVideos);
|
||||
}
|
||||
setProcessedVideos(filteredVideos);
|
||||
} else {
|
||||
console.log('No videos fetched or empty array returned');
|
||||
}
|
||||
|
127
src/components/content/carousels/templates/CombinedTemplate.js
Normal file
127
src/components/content/carousels/templates/CombinedTemplate.js
Normal file
@ -0,0 +1,127 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import Image from "next/image"
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useRouter } from "next/router";
|
||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Tag } from "primereact/tag";
|
||||
import { Message } from "primereact/message";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { PlayCircle, FileText } from "lucide-react";
|
||||
|
||||
export function CombinedTemplate({ resource, isLesson, showMetaTags }) {
|
||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: resource });
|
||||
const [nAddress, setNAddress] = useState(null);
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const windowWidth = useWindowWidth();
|
||||
const isMobile = windowWidth < 768;
|
||||
|
||||
useEffect(() => {
|
||||
if (resource && resource?.d) {
|
||||
const nAddress = nip19.naddrEncode({
|
||||
pubkey: resource.pubkey,
|
||||
kind: resource.kind,
|
||||
identifier: resource.d
|
||||
});
|
||||
setNAddress(nAddress);
|
||||
}
|
||||
}, [resource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, resource);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, resource]);
|
||||
|
||||
const shouldShowMetaTags = (topic) => {
|
||||
if (!showMetaTags) {
|
||||
return !["lesson", "document", "video", "course"].includes(topic);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (zapsError) return <div>Error: {zapsError}</div>;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden group hover:shadow-xl transition-all duration-300 bg-gray-800 m-2 border-none">
|
||||
<div className="relative w-full h-0" style={{ paddingBottom: "56.25%" }}>
|
||||
<Image
|
||||
alt="resource thumbnail"
|
||||
src={returnImageProxy(resource.image)}
|
||||
quality={100}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
||||
<div className="absolute top-4 right-4 flex items-center gap-1 bg-black/50 text-white px-3 py-1 rounded-full">
|
||||
<ZapDisplay zapAmount={zapAmount} event={resource} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||
<PlayCircle className="w-6 h-6 text-white" />
|
||||
<FileText className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row justify-between items-center p-4 border-b border-gray-700">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="text-xl sm:text-2xl text-[#f8f8ff]">{resource.title}</CardTitle>
|
||||
</div>
|
||||
<div>
|
||||
{resource?.price && resource?.price > 0 ? (
|
||||
<Message className={`${isMobile ? "py-1 text-xs" : "py-2"} whitespace-nowrap`} icon="pi pi-lock" severity="info" text={`${resource.price} sats`} />
|
||||
) : (
|
||||
<Message className={`${isMobile ? "py-1 text-xs" : "py-2"} whitespace-nowrap`} icon="pi pi-lock-open" severity="success" text="Free" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className={`${isMobile ? "px-3" : ""} pt-6 pb-2 w-full flex flex-row justify-between items-start`}>
|
||||
<div className="flex flex-wrap gap-2 max-w-[65%]">
|
||||
{resource?.topics?.map((topic, index) => (
|
||||
shouldShowMetaTags(topic) && (
|
||||
<Tag size="small" key={index} className="px-2 py-1 text-sm text-[#f8f8ff]">
|
||||
{topic}
|
||||
</Tag>
|
||||
)
|
||||
))}
|
||||
{isLesson && showMetaTags && <Tag size="small" className="px-2 py-1 text-sm text-[#f8f8ff]" value="lesson" />}
|
||||
</div>
|
||||
<p className="font-bold text-gray-300">Video / Document</p>
|
||||
</CardContent>
|
||||
<CardDescription className={`${isMobile ? "w-full p-3" : "p-6"} py-2 pt-0 text-base text-neutral-50/90 dark:text-neutral-900/90 overflow-hidden min-h-[4em] flex items-center max-w-[100%]`}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "2"
|
||||
}}>
|
||||
<p className="line-clamp-2 text-wrap break-words">{(resource.summary || resource.description)?.split('\n').map((line, index) => (
|
||||
<span className="text-wrap break-words" key={index}>{line}</span>
|
||||
))}</p>
|
||||
</CardDescription>
|
||||
<CardFooter className={`flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-t border-gray-700 pt-4 ${isMobile ? "px-3" : ""}`}>
|
||||
<p className="text-sm text-gray-300">{resource?.published_at && resource.published_at !== "" ? (
|
||||
formatTimestampToHowLongAgo(resource.published_at)
|
||||
) : (
|
||||
formatTimestampToHowLongAgo(resource.created_at)
|
||||
)}</p>
|
||||
<GenericButton
|
||||
onClick={() => router.push(`/details/${nAddress}`)}
|
||||
size="small"
|
||||
label="View"
|
||||
icon="pi pi-chevron-right"
|
||||
iconPos="right"
|
||||
outlined
|
||||
className="items-center py-2"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
254
src/components/content/combined/CombinedDetails.js
Normal file
254
src/components/content/combined/CombinedDetails.js
Normal file
@ -0,0 +1,254 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { Tag } from "primereact/tag";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import ResourcePaymentButton from "@/components/bitcoinConnect/ResourcePaymentButton";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const CombinedDetails = ({ processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, nAddress, handlePaymentSuccess, handlePaymentError, authorView, isLesson }) => {
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const [course, setCourse] = useState(null);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const { zaps, zapsLoading } = useZapsSubscription({ event: processedEvent });
|
||||
const { data: session } = useSession();
|
||||
const { showToast } = useToast();
|
||||
const windowWidth = useWindowWidth();
|
||||
const isMobileView = windowWidth <= 768;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLesson) {
|
||||
axios.get(`/api/resources/${processedEvent.d}`).then(res => {
|
||||
if (res.data && res.data.lessons[0]?.courseId) {
|
||||
setCourse(res.data.lessons[0]?.courseId);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('err', err);
|
||||
});
|
||||
}
|
||||
}, [processedEvent.d, isLesson]);
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, processedEvent);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, processedEvent]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
const response = await axios.delete(`/api/resources/${processedEvent.d}`);
|
||||
if (response.status === 204) {
|
||||
showToast('success', 'Success', 'Resource deleted successfully.');
|
||||
router.push('/');
|
||||
}
|
||||
} catch (error) {
|
||||
if (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 {
|
||||
showToast('error', 'Error', 'Failed to delete resource. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderPaymentMessage = () => {
|
||||
if (session?.user?.role?.subscribed && decryptedContent) {
|
||||
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip="You are subscribed so you can access all paid content" icon="pi pi-check" label="Subscribed" severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
if (isLesson && course && session?.user?.purchased?.some(purchase => purchase.courseId === course)) {
|
||||
const coursePurchase = session?.user?.purchased?.find(purchase => purchase.courseId === course);
|
||||
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip={`You have this lesson through purchasing the course it belongs to. You paid ${coursePurchase?.course?.price} sats for the course.`} icon="pi pi-check" label={`Paid ${coursePurchase?.course?.price} sats`} severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
if (paidResource && decryptedContent && author && processedEvent?.pubkey !== session?.user?.pubkey && !session?.user?.role?.subscribed) {
|
||||
return <GenericButton icon="pi pi-check" label={`Paid ${processedEvent.price} sats`} severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
if (paidResource && author && processedEvent?.pubkey === session?.user?.pubkey) {
|
||||
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip={`You created this paid content, users must pay ${processedEvent.price} sats to access it`} icon="pi pi-check" label={`Price ${processedEvent.price} sats`} severity="success" outlined size="small" className="cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (decryptedContent) {
|
||||
return (
|
||||
<MDDisplay className='p-2 rounded-lg w-full' source={decryptedContent} />
|
||||
);
|
||||
}
|
||||
|
||||
if (paidResource && !decryptedContent) {
|
||||
return (
|
||||
<div className="w-full px-4">
|
||||
<div className="w-full p-8 rounded-lg flex flex-col items-center justify-center bg-gray-800">
|
||||
<div className="mx-auto py-auto">
|
||||
<i className="pi pi-lock text-[60px] text-red-500"></i>
|
||||
</div>
|
||||
<p className="text-center text-xl text-red-500 mt-4">
|
||||
This content is paid and needs to be purchased before viewing.
|
||||
</p>
|
||||
<div className="flex flex-row items-center justify-center w-full mt-4">
|
||||
<ResourcePaymentButton
|
||||
lnAddress={author?.lud16}
|
||||
amount={price}
|
||||
onSuccess={handlePaymentSuccess}
|
||||
onError={handlePaymentError}
|
||||
resourceId={processedEvent.d}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (processedEvent?.content) {
|
||||
return <MDDisplay className='p-2 rounded-lg w-full' source={processedEvent.content} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderAdditionalLinks = () => {
|
||||
if (processedEvent?.additionalLinks?.length > 0) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
<p>Additional Links:</p>
|
||||
{processedEvent.additionalLinks.map((link, index) => (
|
||||
<div key={index} className="mb-2">
|
||||
<a
|
||||
className="text-blue-500 hover:underline hover:text-blue-600 break-words"
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="relative w-full h-[400px] mb-8">
|
||||
<Image
|
||||
alt="background image"
|
||||
src={returnImageProxy(image)}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-20"></div>
|
||||
</div>
|
||||
<div className="w-full mx-auto px-4 py-8 -mt-32 relative z-10 max-mob:px-0 max-tab:px-0">
|
||||
<div className="mb-8 bg-gray-800/70 rounded-lg p-4 max-mob:rounded-t-none max-tab:rounded-t-none">
|
||||
<div className="flex flex-row items-center justify-between w-full">
|
||||
<h1 className='text-4xl font-bold text-white'>{title}</h1>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{topics?.map((topic, index) => (
|
||||
<Tag className='text-[#f8f8ff]' key={index} value={topic} />
|
||||
))}
|
||||
{isLesson && <Tag size="small" className="text-[#f8f8ff]" value="lesson" />}
|
||||
</div>
|
||||
</div>
|
||||
{summary?.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
{renderAdditionalLinks()}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(author?.avatar, author?.username)}
|
||||
width={50}
|
||||
height={50}
|
||||
className="rounded-full mr-4"
|
||||
/>
|
||||
<p className='text-lg text-white'>
|
||||
By{' '}
|
||||
<a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'>
|
||||
{author?.username}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-full mt-8 flex flex-wrap justify-between items-center'>
|
||||
{authorView ? (
|
||||
<div className='flex space-x-2 mt-4 sm:mt-0'>
|
||||
{renderPaymentMessage()}
|
||||
<div className="flex flex-row gap-2">
|
||||
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||
<GenericButton
|
||||
tooltip={isMobileView ? null : "View Nostr Note"}
|
||||
tooltipOptions={{ position: 'left' }}
|
||||
icon="pi pi-external-link"
|
||||
outlined
|
||||
onClick={() => window.open(`https://habla.news/a/${nAddress}`, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex flex-row justify-between gap-2">
|
||||
{renderPaymentMessage()}
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{course && (
|
||||
<GenericButton
|
||||
size={isMobileView ? 'small' : null}
|
||||
outlined
|
||||
icon="pi pi-external-link"
|
||||
onClick={() => window.open(`/course/${course}`, '_blank')}
|
||||
label={isMobileView ? "Course" : "Open Course"}
|
||||
tooltip="This is a lesson in a course"
|
||||
tooltipOptions={{ position: 'top' }}
|
||||
/>
|
||||
)}
|
||||
<GenericButton
|
||||
size={isMobileView ? 'small' : null}
|
||||
tooltip={isMobileView ? null : "View Nostr Note"}
|
||||
tooltipOptions={{ position: 'left' }}
|
||||
icon="pi pi-external-link"
|
||||
outlined
|
||||
onClick={() => window.open(`https://habla.news/a/${nAddress}`, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombinedDetails;
|
259
src/components/content/courses/CombinedLesson.js
Normal file
259
src/components/content/courses/CombinedLesson.js
Normal file
@ -0,0 +1,259 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { Tag } from "primereact/tag";
|
||||
import Image from "next/image";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Divider } from "primereact/divider";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import dynamic from "next/dynamic";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import appConfig from "@/config/appConfig";
|
||||
import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson';
|
||||
|
||||
const MDDisplay = dynamic(
|
||||
() => import("@uiw/react-markdown-preview"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => {
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const [nAddress, setNAddress] = useState(null);
|
||||
const [videoDuration, setVideoDuration] = useState(null);
|
||||
const [videoPlayed, setVideoPlayed] = useState(false);
|
||||
const mdDisplayRef = useRef(null);
|
||||
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const windowWidth = useWindowWidth();
|
||||
const isMobileView = windowWidth <= 768;
|
||||
const isVideo = lesson?.type === 'video';
|
||||
|
||||
const { isCompleted: videoCompleted, isTracking: videoTracking } = useTrackVideoLesson({
|
||||
lessonId: lesson?.d,
|
||||
videoDuration,
|
||||
courseId: course?.d,
|
||||
videoPlayed,
|
||||
paidCourse: isPaid,
|
||||
decryptionPerformed
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleYouTubeMessage = (event) => {
|
||||
if (event.origin !== "https://www.youtube.com") return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.event === "onReady") {
|
||||
event.source.postMessage('{"event":"listening"}', "https://www.youtube.com");
|
||||
} else if (data.event === "infoDelivery" && data?.info?.currentTime) {
|
||||
setVideoPlayed(true);
|
||||
setVideoDuration(data.info?.progressState?.duration);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing YouTube message:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (isVideo) {
|
||||
window.addEventListener("message", handleYouTubeMessage);
|
||||
return () => window.removeEventListener("message", handleYouTubeMessage);
|
||||
}
|
||||
}, [isVideo]);
|
||||
|
||||
const checkDuration = useCallback(() => {
|
||||
if (!isVideo) return;
|
||||
|
||||
const videoElement = mdDisplayRef.current?.querySelector('video');
|
||||
const youtubeIframe = mdDisplayRef.current?.querySelector('iframe[src*="youtube.com"]');
|
||||
|
||||
if (videoElement && videoElement.readyState >= 1) {
|
||||
setVideoDuration(Math.round(videoElement.duration));
|
||||
setVideoPlayed(true);
|
||||
} else if (youtubeIframe) {
|
||||
youtubeIframe.contentWindow.postMessage('{"event":"listening"}', '*');
|
||||
}
|
||||
}, [isVideo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!zaps || zapsLoading || zapsError) return;
|
||||
const total = getTotalFromZaps(zaps, lesson);
|
||||
setZapAmount(total);
|
||||
}, [zaps, zapsLoading, zapsError, lesson]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lesson) {
|
||||
const addr = nip19.naddrEncode({
|
||||
pubkey: lesson.pubkey,
|
||||
kind: lesson.kind,
|
||||
identifier: lesson.d,
|
||||
relays: appConfig.defaultRelayUrls
|
||||
});
|
||||
setNAddress(addr);
|
||||
}
|
||||
}, [lesson]);
|
||||
|
||||
useEffect(() => {
|
||||
if (decryptionPerformed && isPaid) {
|
||||
const timer = setTimeout(checkDuration, 500);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
const timer = setTimeout(checkDuration, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [decryptionPerformed, isPaid, checkDuration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVideo && videoCompleted && !videoTracking) {
|
||||
setCompleted(lesson.id);
|
||||
}
|
||||
}, [videoCompleted, videoTracking, lesson.id, setCompleted, isVideo]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isPaid && decryptionPerformed) {
|
||||
return (
|
||||
<div ref={mdDisplayRef}>
|
||||
<MDDisplay className={`${isVideo ? 'p-0' : 'p-2'} rounded-lg w-full`} source={lesson.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPaid && !decryptionPerformed) {
|
||||
if (isVideo) {
|
||||
return (
|
||||
<div className="w-full aspect-video rounded-lg flex flex-col items-center justify-center relative overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-50"
|
||||
style={{
|
||||
backgroundImage: `url(${lesson.image})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
<div className="mx-auto py-auto z-10">
|
||||
<i className="pi pi-lock text-[100px] text-red-500"></i>
|
||||
</div>
|
||||
<p className="text-center text-xl text-red-500 z-10 mt-4">
|
||||
This content is paid and needs to be purchased before viewing.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-full p-8 rounded-lg flex flex-col items-center justify-center bg-gray-100 dark:bg-gray-800">
|
||||
<div className="mx-auto py-auto">
|
||||
<i className="pi pi-lock text-[60px] text-red-500"></i>
|
||||
</div>
|
||||
<p className="text-center text-xl text-red-500 mt-4">
|
||||
This content is paid and needs to be purchased before viewing.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (lesson?.content) {
|
||||
return (
|
||||
<div ref={mdDisplayRef}>
|
||||
<MDDisplay className={`${isVideo ? 'p-0' : 'p-2'} rounded-lg w-full`} source={lesson.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{isVideo ? renderContent() : (
|
||||
<>
|
||||
<div className="relative w-[80%] h-[200px] mx-auto mb-24">
|
||||
<Image
|
||||
alt="lesson background image"
|
||||
src={returnImageProxy(lesson.image)}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-20"></div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={`${isVideo ? 'bg-gray-800/90 rounded-lg p-4 m-4' : 'w-full mx-auto px-4 py-8 -mt-32 relative z-10'}`}>
|
||||
<div className={`${!isVideo && 'mb-8 bg-gray-800/70 rounded-lg p-4'}`}>
|
||||
<div className="flex flex-row items-center justify-between w-full">
|
||||
<h1 className='text-3xl font-bold text-white'>{lesson.title}</h1>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{lesson.topics && lesson.topics.length > 0 && (
|
||||
lesson.topics.map((topic, index) => (
|
||||
<Tag className='text-white' key={index} value={topic}></Tag>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xl text-gray-200 mb-4 mt-4'>{lesson.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{lesson.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(lesson.author?.avatar, lesson.author?.username)}
|
||||
width={50}
|
||||
height={50}
|
||||
className="rounded-full mr-4"
|
||||
/>
|
||||
<p className='text-lg text-white'>
|
||||
By{' '}
|
||||
<a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'>
|
||||
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={lesson}
|
||||
zapsLoading={zapsLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end">
|
||||
<GenericButton
|
||||
tooltip={isMobileView ? null : "View Nostr Note"}
|
||||
tooltipOptions={{ position: 'left' }}
|
||||
icon="pi pi-external-link"
|
||||
outlined
|
||||
onClick={() => {
|
||||
window.open(`https://habla.news/a/${nAddress}`, '_blank');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isVideo && <Divider />}
|
||||
{lesson?.additionalLinks && lesson.additionalLinks.length > 0 && (
|
||||
<div className='mt-6 bg-gray-800/90 rounded-lg p-4'>
|
||||
<h3 className='text-lg font-semibold mb-2 text-white'>External links:</h3>
|
||||
<ul className='list-disc list-inside text-white'>
|
||||
{lesson.additionalLinks.map((link, index) => (
|
||||
<li key={index}>
|
||||
<a href={link} target="_blank" rel="noopener noreferrer" className='text-blue-300 hover:underline'>
|
||||
{new URL(link).hostname}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isVideo && renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombinedLesson;
|
294
src/components/forms/CombinedResourceForm.js
Normal file
294
src/components/forms/CombinedResourceForm.js
Normal file
@ -0,0 +1,294 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'next/router';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { InputSwitch } from 'primereact/inputswitch';
|
||||
import GenericButton from '@/components/buttons/GenericButton';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import { useEncryptContent } from '@/hooks/encryption/useEncryptContent';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import 'primeicons/primeicons.css';
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
|
||||
const MDEditor = dynamic(
|
||||
() => import("@uiw/react-md-editor"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const CombinedResourceForm = ({ draft = null, isPublished = false }) => {
|
||||
const [title, setTitle] = useState(draft?.title || '');
|
||||
const [summary, setSummary] = useState(draft?.summary || '');
|
||||
const [price, setPrice] = useState(draft?.price || 0);
|
||||
const [isPaidResource, setIsPaidResource] = useState(draft?.price ? true : false);
|
||||
const [videoUrl, setVideoUrl] = useState(draft?.videoUrl || '');
|
||||
const [content, setContent] = useState(draft?.content || '');
|
||||
const [coverImage, setCoverImage] = useState(draft?.image || '');
|
||||
const [topics, setTopics] = useState(draft?.topics || ['']);
|
||||
const [additionalLinks, setAdditionalLinks] = useState(draft?.additionalLinks || ['']);
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const { showToast } = useToast();
|
||||
const { ndk, addSigner } = useNDKContext();
|
||||
const { encryptContent } = useEncryptContent();
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
setUser(session.user);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const handleContentChange = useCallback((value) => {
|
||||
setContent(value || '');
|
||||
}, []);
|
||||
|
||||
const getVideoEmbed = (url) => {
|
||||
let embedCode = '';
|
||||
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
const videoId = url.split('v=')[1] || url.split('/').pop();
|
||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}?enablejsapi=1" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
||||
} else if (url.includes('vimeo.com')) {
|
||||
const videoId = url.split('/').pop();
|
||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://player.vimeo.com/video/${videoId}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
||||
} else if (url.includes('.mp4') || url.includes('.mov') || url.includes('.avi') || url.includes('.wmv') || url.includes('.flv') || url.includes('.webm')) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
|
||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><video src="${baseUrl}/api/get-video-url?videoKey=${encodeURIComponent(url)}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" controls></video></div>`;
|
||||
}
|
||||
|
||||
return embedCode;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const userResponse = await axios.get(`/api/users/${user.pubkey}`);
|
||||
if (!userResponse.data) {
|
||||
showToast('error', 'Error', 'User not found', 'Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoEmbed = videoUrl ? getVideoEmbed(videoUrl) : '';
|
||||
const combinedContent = `${videoEmbed}\n\n${content}`;
|
||||
|
||||
const payload = {
|
||||
title,
|
||||
summary,
|
||||
type: 'combined',
|
||||
price: isPaidResource ? price : null,
|
||||
content: combinedContent,
|
||||
image: coverImage,
|
||||
user: userResponse.data.id,
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'video', 'document'])],
|
||||
additionalLinks: additionalLinks.filter(link => link.trim() !== ''),
|
||||
};
|
||||
|
||||
const url = draft ? `/api/drafts/${draft.id}` : '/api/drafts';
|
||||
const method = draft ? 'put' : 'post';
|
||||
|
||||
try {
|
||||
const response = await axios[method](url, payload);
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
showToast('success', 'Success', draft ? 'Content updated successfully.' : 'Content saved as draft.');
|
||||
if (response.data?.id) {
|
||||
router.push(`/draft/${response.data.id}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('error', 'Error', 'Failed to save content. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTopicChange = (index, value) => {
|
||||
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
|
||||
setTopics(updatedTopics);
|
||||
};
|
||||
|
||||
const addTopic = (e) => {
|
||||
e.preventDefault();
|
||||
setTopics([...topics, '']);
|
||||
};
|
||||
|
||||
const removeTopic = (e, index) => {
|
||||
e.preventDefault();
|
||||
const updatedTopics = topics.filter((_, i) => i !== index);
|
||||
setTopics(updatedTopics);
|
||||
};
|
||||
|
||||
const handleAdditionalLinkChange = (index, value) => {
|
||||
const updatedAdditionalLinks = additionalLinks.map((link, i) => i === index ? value : link);
|
||||
setAdditionalLinks(updatedAdditionalLinks);
|
||||
};
|
||||
|
||||
const addAdditionalLink = (e) => {
|
||||
e.preventDefault();
|
||||
setAdditionalLinks([...additionalLinks, '']);
|
||||
};
|
||||
|
||||
const removeAdditionalLink = (e, index) => {
|
||||
e.preventDefault();
|
||||
const updatedAdditionalLinks = additionalLinks.filter((_, i) => i !== index);
|
||||
setAdditionalLinks(updatedAdditionalLinks);
|
||||
};
|
||||
|
||||
const buildEvent = async (draft) => {
|
||||
const dTag = draft.d;
|
||||
const event = new NDKEvent(ndk);
|
||||
let encryptedContent;
|
||||
|
||||
const videoEmbed = videoUrl ? getVideoEmbed(videoUrl) : '';
|
||||
const combinedContent = `${videoEmbed}\n\n${content}`;
|
||||
|
||||
if (draft?.price) {
|
||||
encryptedContent = await encryptContent(combinedContent);
|
||||
}
|
||||
|
||||
event.kind = draft?.price ? 30402 : 30023;
|
||||
event.content = draft?.price ? encryptedContent : combinedContent;
|
||||
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();
|
||||
|
||||
const updatedDraft = {
|
||||
title,
|
||||
summary,
|
||||
price,
|
||||
content,
|
||||
videoUrl,
|
||||
d: draft.d,
|
||||
image: coverImage,
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'video', 'document'])],
|
||||
additionalLinks: additionalLinks.filter(link => link.trim() !== '')
|
||||
};
|
||||
|
||||
const event = await buildEvent(updatedDraft);
|
||||
|
||||
try {
|
||||
if (!ndk.signer) {
|
||||
await addSigner();
|
||||
}
|
||||
|
||||
await ndk.connect();
|
||||
|
||||
const published = await ndk.publish(event);
|
||||
|
||||
if (published) {
|
||||
const response = await axios.put(`/api/resources/${draft.d}`, { noteId: event.id });
|
||||
showToast('success', 'Success', 'Content published successfully.');
|
||||
router.push(`/details/${event.id}`);
|
||||
} else {
|
||||
showToast('error', 'Error', 'Failed to publish content. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('error', 'Error', 'Failed to publish content. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="p-inputgroup flex-1">
|
||||
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputTextarea value={summary} onChange={(e) => setSummary(e.target.value)} placeholder="Summary" rows={5} cols={30} />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4 flex-col">
|
||||
<p className="py-2">Paid Resource</p>
|
||||
<InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
|
||||
{isPaidResource && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
<i className="pi pi-bolt p-inputgroup-addon text-2xl text-yellow-500"></i>
|
||||
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputText value={videoUrl} onChange={(e) => setVideoUrl(e.target.value)} placeholder="Video URL (YouTube, Vimeo, or direct video link)" />
|
||||
</div>
|
||||
|
||||
<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 flex-col mt-4">
|
||||
<span>Content</span>
|
||||
<div data-color-mode="dark">
|
||||
<MDEditor
|
||||
value={content}
|
||||
onChange={handleContentChange}
|
||||
height={350}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex-col w-full">
|
||||
<span className="pl-1 flex items-center">
|
||||
External Links
|
||||
<i className="pi pi-info-circle ml-2 cursor-pointer"
|
||||
data-pr-tooltip="Add any relevant external links that pair with this content (these links are currently not encrypted for 'paid' content)"
|
||||
data-pr-position="right"
|
||||
data-pr-at="right+5 top"
|
||||
data-pr-my="left center-2"
|
||||
style={{ fontSize: '1rem', color: 'var(--primary-color)' }}
|
||||
/>
|
||||
</span>
|
||||
{additionalLinks.map((link, index) => (
|
||||
<div className="p-inputgroup flex-1" key={index}>
|
||||
<InputText value={link} onChange={(e) => handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" />
|
||||
{index > 0 && (
|
||||
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="w-full flex flex-row items-end justify-end py-2">
|
||||
<GenericButton icon="pi pi-plus" onClick={addAdditionalLink} />
|
||||
</div>
|
||||
<Tooltip target=".pi-info-circle" />
|
||||
</div>
|
||||
<div className="mt-8 flex-col w-full">
|
||||
{topics.map((topic, index) => (
|
||||
<div className="p-inputgroup flex-1" key={index}>
|
||||
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
|
||||
{index > 0 && (
|
||||
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="w-full flex flex-row items-end justify-end py-2">
|
||||
<GenericButton icon="pi pi-plus" onClick={addTopic} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-8">
|
||||
<GenericButton type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CombinedResourceForm;
|
@ -67,6 +67,11 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
value: content
|
||||
}));
|
||||
|
||||
const combinedOptions = filteredContent.filter(content => content?.topics?.includes('video') && content?.topics?.includes('document') && content.kind).map(content => ({
|
||||
label: content.title,
|
||||
value: content
|
||||
}));
|
||||
|
||||
setContentOptions([
|
||||
{
|
||||
label: 'Draft Documents',
|
||||
@ -83,6 +88,10 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
{
|
||||
label: 'Published Videos',
|
||||
items: videoOptions
|
||||
},
|
||||
{
|
||||
label: 'Published Combined',
|
||||
items: combinedOptions
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { parseCourseEvent, parseEvent, findKind0Fields } from "@/utils/nostr";
|
||||
import CourseDetails from "@/components/content/courses/CourseDetails";
|
||||
import VideoLesson from "@/components/content/courses/VideoLesson";
|
||||
import DocumentLesson from "@/components/content/courses/DocumentLesson";
|
||||
import CombinedLesson from "@/components/content/courses/CombinedLesson";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useSession } from 'next-auth/react';
|
||||
import axios from "axios";
|
||||
@ -225,6 +226,16 @@ const Course = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const renderLesson = (lesson) => {
|
||||
if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) {
|
||||
return <CombinedLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} />;
|
||||
} else if (lesson.type === 'video' && !lesson.topics?.includes('document')) {
|
||||
return <VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} />;
|
||||
} else if (lesson.type === 'document' && !lesson.topics?.includes('video')) {
|
||||
return <DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{course && paidCourse !== null && (
|
||||
@ -260,10 +271,7 @@ const Course = () => {
|
||||
}
|
||||
>
|
||||
<div className="w-full py-4 rounded-b-lg">
|
||||
{lesson.type === 'video' ?
|
||||
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} /> :
|
||||
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} setCompleted={setCompleted} />
|
||||
}
|
||||
{renderLesson(lesson)}
|
||||
</div>
|
||||
</AccordionTab>
|
||||
))}
|
||||
|
@ -3,6 +3,7 @@ import MenuTab from "@/components/menutab/MenuTab";
|
||||
import DocumentForm from "@/components/forms/DocumentForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import CombinedResourceForm from "@/components/forms/CombinedResourceForm";
|
||||
import { useIsAdmin } from "@/hooks/useIsAdmin";
|
||||
import { useRouter } from "next/router";
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
@ -14,6 +15,7 @@ const Create = () => {
|
||||
const homeItems = [
|
||||
{ label: 'Document', icon: 'pi pi-file' },
|
||||
{ label: 'Video', icon: 'pi pi-video' },
|
||||
{ label: 'Combined', icon: 'pi pi-clone' },
|
||||
{ label: 'Course', icon: 'pi pi-desktop' }
|
||||
];
|
||||
|
||||
@ -34,8 +36,10 @@ const Create = () => {
|
||||
return <VideoForm />;
|
||||
case 'Document':
|
||||
return <DocumentForm />;
|
||||
case 'Combined':
|
||||
return <CombinedResourceForm />;
|
||||
default:
|
||||
return null; // or a default component
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { parseEvent } from "@/utils/nostr";
|
||||
import DocumentForm from "@/components/forms/DocumentForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import CombinedResourceForm from "@/components/forms/CombinedResourceForm";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
|
||||
@ -39,8 +40,9 @@ export default function Edit() {
|
||||
<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('video') && <VideoForm draft={event} isPublished />}
|
||||
{event?.topics.includes('document') && <DocumentForm draft={event} isPublished />}
|
||||
{event?.topics.includes('video') && !event?.topics.includes('document') && <VideoForm draft={event} isPublished />}
|
||||
{event?.topics.includes('document') && !event?.topics.includes('video') && <DocumentForm draft={event} isPublished />}
|
||||
{event?.topics.includes('video') && event?.topics.includes('document') && <CombinedResourceForm draft={event} isPublished />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { useRouter } from "next/router";
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import axios from 'axios';
|
||||
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
||||
import CombinedDetails from "@/components/content/combined/CombinedDetails";
|
||||
|
||||
// todo: /decrypt is still being called way too much on this page, need to clean up state management
|
||||
|
||||
@ -138,7 +139,7 @@ const Details = () => {
|
||||
};
|
||||
|
||||
fetchAndProcessEvent();
|
||||
}, [router.isReady, router.query, ndk, session, decryptContent, fetchAuthor, showToast]);
|
||||
}, [router.isReady, router.query, ndk, session]);
|
||||
|
||||
const handlePaymentSuccess = (response) => {
|
||||
if (response && response?.preimage) {
|
||||
@ -158,7 +159,14 @@ const Details = () => {
|
||||
|
||||
if (!author || !event) return null;
|
||||
|
||||
const DetailComponent = event.type === "document" ? DocumentDetails : VideoDetails;
|
||||
const getDetailComponent = () => {
|
||||
if (event.topics.includes('video') && event.topics.includes('document')) {
|
||||
return CombinedDetails;
|
||||
}
|
||||
return event.type === "document" ? DocumentDetails : VideoDetails;
|
||||
};
|
||||
|
||||
const DetailComponent = getDetailComponent();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -4,6 +4,7 @@ import axios from "axios";
|
||||
import DocumentForm from "@/components/forms/DocumentForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import CombinedResourceForm from "@/components/forms/CombinedResourceForm";
|
||||
import { useIsAdmin } from "@/hooks/useIsAdmin";
|
||||
|
||||
const Edit = () => {
|
||||
@ -38,6 +39,7 @@ const Edit = () => {
|
||||
{draft?.type === 'course' && <CourseForm draft={draft} />}
|
||||
{draft?.type === 'video' && <VideoForm draft={draft} />}
|
||||
{draft?.type === 'document' && <DocumentForm draft={draft} />}
|
||||
{draft?.type === 'combined' && <CombinedResourceForm draft={draft} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -116,8 +116,8 @@ export default function Draft() {
|
||||
|
||||
const handlePostResource = async (resource, videoId) => {
|
||||
const dTag = resource.tags.find(tag => tag[0] === 'd')[1];
|
||||
let price
|
||||
|
||||
let price
|
||||
|
||||
try {
|
||||
price = resource.tags.find(tag => tag[0] === 'price')[1];
|
||||
} catch (err) {
|
||||
@ -241,6 +241,33 @@ export default function Draft() {
|
||||
|
||||
type = 'video';
|
||||
break;
|
||||
case 'combined':
|
||||
if (draft?.price) {
|
||||
encryptedContent = await encryptContent(draft.content);
|
||||
}
|
||||
|
||||
if (draft?.content.includes('?videoKey=')) {
|
||||
const extractedVideoId = draft.content.split('?videoKey=')[1].split('"')[0];
|
||||
videoId = extractedVideoId;
|
||||
}
|
||||
|
||||
event.kind = draft?.price ? 30402 : 30023;
|
||||
event.content = draft?.price ? encryptedContent : 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()],
|
||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||
...(draft?.additionalLinks ? draft.additionalLinks.filter(link => link !== 'https://plebdevs.com').map(link => ['r', link]) : []),
|
||||
];
|
||||
|
||||
type = 'combined';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -264,13 +291,13 @@ export default function Draft() {
|
||||
</div>
|
||||
<h1 className='text-4xl mt-4'>{draft?.title}</h1>
|
||||
<p className='text-xl mt-4'>{draft?.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{draft.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<div className="text-xl mt-4">
|
||||
{draft.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
{draft?.price && (
|
||||
<p className='text-lg mt-4'>Price: {draft.price}</p>
|
||||
)}
|
||||
@ -305,7 +332,7 @@ export default function Draft() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="pt-8 text-sm text-gray-400">{draft?.createdAt && formatDateTime(draft?.createdAt)}</p>
|
||||
<p className="pt-8 text-sm text-gray-400">{draft?.createdAt && formatDateTime(draft?.createdAt)}</p>
|
||||
</div>
|
||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
||||
{draft && (
|
||||
|
Loading…
x
Reference in New Issue
Block a user