mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
New content templates and content details components
This commit is contained in:
parent
e825803077
commit
cf1f1d73c3
@ -5,6 +5,7 @@ import { initializeBitcoinConnect } from './BitcoinConnect';
|
|||||||
import { LightningAddress } from '@getalby/lightning-tools';
|
import { LightningAddress } from '@getalby/lightning-tools';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import GenericButton from '@/components/buttons/GenericButton';
|
import GenericButton from '@/components/buttons/GenericButton';
|
||||||
|
|
||||||
@ -73,15 +74,24 @@ const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resource
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GenericButton
|
{
|
||||||
label={`${amount} sats`}
|
invoice ? (
|
||||||
icon="pi pi-wallet"
|
<GenericButton
|
||||||
onClick={() => setDialogVisible(true)}
|
label={`${amount} sats`}
|
||||||
disabled={!invoice}
|
icon="pi pi-wallet"
|
||||||
severity='primary'
|
onClick={() => setDialogVisible(true)}
|
||||||
rounded
|
disabled={!invoice}
|
||||||
className="text-[#f8f8ff] text-sm"
|
severity='primary'
|
||||||
/>
|
rounded
|
||||||
|
className="text-[#f8f8ff] text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ProgressSpinner
|
||||||
|
style={{ width: '30px', height: '30px' }}
|
||||||
|
strokeWidth="8"
|
||||||
|
animationDuration=".5s"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Dialog
|
<Dialog
|
||||||
visible={dialogVisible}
|
visible={dialogVisible}
|
||||||
onHide={() => setDialogVisible(false)}
|
onHide={() => setDialogVisible(false)}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { Carousel } from 'primereact/carousel';
|
import { Carousel } from 'primereact/carousel';
|
||||||
import ResourceTemplate from '@/components/content/carousels/templates/ResourceTemplate';
|
|
||||||
import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate';
|
|
||||||
import WorkshopTemplate from '@/components/content/carousels/templates/WorkshopTemplate';
|
|
||||||
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
||||||
|
import { VideoTemplate } from '@/components/content/carousels/newTemplates/VideoTemplate';
|
||||||
|
import { DocumentTemplate } from '@/components/content/carousels/newTemplates/DocumentTemplate';
|
||||||
|
import { CourseTemplate } from '@/components/content/carousels/newTemplates/CourseTemplate';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
const responsiveOptions = [
|
const responsiveOptions = [
|
||||||
{
|
{
|
||||||
@ -23,32 +24,37 @@ const responsiveOptions = [
|
|||||||
export default function GenericCarousel({items, selectedTopic, title}) {
|
export default function GenericCarousel({items, selectedTopic, title}) {
|
||||||
const [carousels, setCarousels] = useState([]);
|
const [carousels, setCarousels] = useState([]);
|
||||||
|
|
||||||
|
const memoizedItems = useMemo(() => items, [items]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
console.log("carousel update", carousels);
|
||||||
const width = window.innerWidth;
|
}, [carousels]);
|
||||||
let itemsPerCarousel = 3;
|
|
||||||
|
|
||||||
if (width <= 1462) {
|
const getItemsPerCarousel = useCallback(() => {
|
||||||
itemsPerCarousel = 2;
|
const width = window.innerWidth;
|
||||||
}
|
if (width <= 575) return 1;
|
||||||
if (width <= 575) {
|
if (width <= 1462) return 2;
|
||||||
itemsPerCarousel = 1;
|
return 3;
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
const newCarousels = [];
|
const updateCarousels = useCallback(() => {
|
||||||
for (let i = 0; i < items.length; i += itemsPerCarousel) {
|
const itemsPerCarousel = getItemsPerCarousel();
|
||||||
newCarousels.push(items.slice(i, i + itemsPerCarousel));
|
const newCarousels = [];
|
||||||
}
|
for (let i = 0; i < memoizedItems.length; i += itemsPerCarousel) {
|
||||||
setCarousels(newCarousels);
|
newCarousels.push(memoizedItems.slice(i, i + itemsPerCarousel));
|
||||||
};
|
}
|
||||||
|
setCarousels(newCarousels);
|
||||||
|
}, [memoizedItems, getItemsPerCarousel]);
|
||||||
|
|
||||||
handleResize();
|
useEffect(() => {
|
||||||
window.addEventListener('resize', handleResize);
|
updateCarousels();
|
||||||
|
const debouncedHandleResize = debounce(updateCarousels, 250);
|
||||||
|
window.addEventListener('resize', debouncedHandleResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', debouncedHandleResize);
|
||||||
};
|
};
|
||||||
}, [items]);
|
}, [updateCarousels, memoizedItems]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -59,9 +65,9 @@ export default function GenericCarousel({items, selectedTopic, title}) {
|
|||||||
itemTemplate={(item) => {
|
itemTemplate={(item) => {
|
||||||
if (carouselItems.length > 0) {
|
if (carouselItems.length > 0) {
|
||||||
if (item.type === 'resource') {
|
if (item.type === 'resource') {
|
||||||
return <ResourceTemplate key={item.id} resource={item} />;
|
return <DocumentTemplate key={item.id} document={item} />;
|
||||||
} else if (item.type === 'workshop') {
|
} else if (item.type === 'workshop') {
|
||||||
return <WorkshopTemplate key={item.id} workshop={item} />;
|
return <VideoTemplate key={item.id} video={item} />;
|
||||||
} else if (item.type === 'course') {
|
} else if (item.type === 'course') {
|
||||||
return <CourseTemplate key={item.id} course={item} />;
|
return <CourseTemplate key={item.id} course={item} />;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import Image from "next/image"
|
|||||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||||
import { getTotalFromZaps } from "@/utils/lightning";
|
import { getTotalFromZaps } from "@/utils/lightning";
|
||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/router";
|
||||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||||
import GenericButton from "@/components/buttons/GenericButton";
|
import GenericButton from "@/components/buttons/GenericButton";
|
||||||
|
|
||||||
@ -42,17 +42,17 @@ export function CourseTemplate({ course }) {
|
|||||||
alt="Course background"
|
alt="Course background"
|
||||||
quality={100}
|
quality={100}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
className={`${router.pathname === "/content" ? "w-full h-full object-cover" : "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"}`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
<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-2 py-1 rounded-full z-10">
|
<div className="absolute top-4 right-4 flex items-center gap-1 bg-black/50 text-white px-2 py-1 rounded-full z-10">
|
||||||
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading && zapAmount === 0} />
|
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[50%] max-h-[50%]">
|
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[70%] max-h-[60%]">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<BookOpen className="w-10 h-10 mt-1 text-neutral-50 dark:text-neutral-900" />
|
<BookOpen className="w-10 h-10 mt-1 text-neutral-50 dark:text-neutral-900" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-2xl sm:text-3xl font-bold mb-2">{course.name || course.title}</CardTitle>
|
<CardTitle className="text-2xl sm:text-3xl mb-2">{course.name || course.title}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -82,7 +82,7 @@ export function CourseTemplate({ course }) {
|
|||||||
) : (
|
) : (
|
||||||
formatTimestampToHowLongAgo(course.created_at)
|
formatTimestampToHowLongAgo(course.created_at)
|
||||||
)}</p>
|
)}</p>
|
||||||
<GenericButton onClick={() => router.push(`/details/${course.id}`)} size="small" label="Start Learning" icon="pi pi-chevron-right" iconPos="right" outlined className="items-center py-2" />
|
<GenericButton onClick={() => router.push(`/course/${course.id}`)} size="small" label="Start Learning" icon="pi pi-chevron-right" iconPos="right" outlined className="items-center py-2" />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ import Image from "next/image"
|
|||||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||||
import { getTotalFromZaps } from "@/utils/lightning";
|
import { getTotalFromZaps } from "@/utils/lightning";
|
||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/router";
|
||||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||||
import { Tag } from "primereact/tag";
|
import { Tag } from "primereact/tag";
|
||||||
import GenericButton from "@/components/buttons/GenericButton";
|
import GenericButton from "@/components/buttons/GenericButton";
|
||||||
@ -34,17 +34,17 @@ export function DocumentTemplate({ document }) {
|
|||||||
alt="Document background"
|
alt="Document background"
|
||||||
quality={100}
|
quality={100}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
className={`${router.pathname === "/content" ? "w-full h-full object-cover" : "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"}`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
<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">
|
<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={document} zapsLoading={zapsLoading && zapAmount === 0} />
|
<ZapDisplay zapAmount={zapAmount} event={document} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[50%] max-h-[50%]">
|
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[70%] max-h-[60%]">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<FileText className="w-10 h-10 mt-1 text-neutral-50 dark:text-neutral-900" />
|
<FileText className="w-10 h-10 mt-1 text-neutral-50 dark:text-neutral-900" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-2xl sm:text-3xl font-bold mb-2">{document.title}</CardTitle>
|
<CardTitle className="text-2xl sm:text-3xl mb-2">{document.title}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
@ -6,7 +6,7 @@ import Image from "next/image"
|
|||||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||||
import { getTotalFromZaps } from "@/utils/lightning";
|
import { getTotalFromZaps } from "@/utils/lightning";
|
||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/router";
|
||||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||||
import { Tag } from "primereact/tag";
|
import { Tag } from "primereact/tag";
|
||||||
import GenericButton from "@/components/buttons/GenericButton";
|
import GenericButton from "@/components/buttons/GenericButton";
|
||||||
@ -34,17 +34,17 @@ export function VideoTemplate({ video }) {
|
|||||||
alt="Video thumbnail"
|
alt="Video thumbnail"
|
||||||
quality={100}
|
quality={100}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
className={`${router.pathname === "/content" ? "w-full h-full object-cover" : "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"}`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
<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">
|
<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={video} zapsLoading={zapsLoading && zapAmount === 0} />
|
<ZapDisplay zapAmount={zapAmount} event={video} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||||
</div>
|
</div>
|
||||||
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[50%] max-h-[50%]">
|
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[70%] max-h-[60%]">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<PlayCircle className="w-10 h-10 mt-1 text-neutral-50 dark:text-neutral-900" />
|
<PlayCircle className="w-10 h-10 mt-1 text-neutral-50 dark:text-neutral-900" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-2xl sm:text-3xl font-bold mb-2">{video.title}</CardTitle>
|
<CardTitle className="text-2xl sm:text-3xl mb-2">{video.title}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
159
src/components/content/courses/CourseDetailsNew.js
Normal file
159
src/components/content/courses/CourseDetailsNew.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } 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 CoursePaymentButton from "@/components/bitcoinConnect/CoursePaymentButton";
|
||||||
|
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';
|
||||||
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
|
import { findKind0Fields } from '@/utils/nostr';
|
||||||
|
|
||||||
|
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
|
||||||
|
|
||||||
|
export default function CourseDetailsNew({ processedEvent, paidCourse, lessons, decryptionPerformed, handlePaymentSuccess, handlePaymentError }) {
|
||||||
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
|
const [author, setAuthor] = useState(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const windowWidth = useWindowWidth();
|
||||||
|
const { ndk } = useNDKContext();
|
||||||
|
|
||||||
|
const fetchAuthor = useCallback(async (pubkey) => {
|
||||||
|
if (!pubkey) return;
|
||||||
|
const author = await ndk.getUser({ pubkey });
|
||||||
|
const profile = await author.fetchProfile();
|
||||||
|
const fields = await findKind0Fields(profile);
|
||||||
|
if (fields) {
|
||||||
|
setAuthor(fields);
|
||||||
|
}
|
||||||
|
}, [ndk]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (processedEvent) {
|
||||||
|
fetchAuthor(processedEvent.pubkey);
|
||||||
|
}
|
||||||
|
}, [fetchAuthor, processedEvent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (zaps.length > 0) {
|
||||||
|
const total = getTotalFromZaps(zaps, processedEvent);
|
||||||
|
setZapAmount(total);
|
||||||
|
}
|
||||||
|
}, [zaps, processedEvent]);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/courses/${processedEvent.d}`);
|
||||||
|
if (response.status === 204) {
|
||||||
|
showToast('success', 'Success', 'Course deleted successfully.');
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('error', 'Error', 'Failed to delete course. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPaymentMessage = () => {
|
||||||
|
if (session?.user && session.user?.role?.subscribed && decryptionPerformed) {
|
||||||
|
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 (paidCourse && decryptionPerformed && 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 (paidCourse && author && processedEvent?.pubkey === session?.user?.pubkey) {
|
||||||
|
return <GenericButton tooltipOptions={{ position: 'top' }} tooltip={`You created this paid course, 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" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paidCourse && !decryptionPerformed) {
|
||||||
|
return (
|
||||||
|
<CoursePaymentButton
|
||||||
|
lnAddress={lnAddress}
|
||||||
|
amount={processedEvent.price}
|
||||||
|
onSuccess={handlePaymentSuccess}
|
||||||
|
onError={handlePaymentError}
|
||||||
|
courseId={processedEvent.d}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!processedEvent || !author) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="relative w-full h-[400px] mb-8">
|
||||||
|
<Image
|
||||||
|
alt="course image"
|
||||||
|
src={returnImageProxy(processedEvent.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">
|
||||||
|
<i className={`pi pi-arrow-left cursor-pointer hover:opacity-75 absolute top-0 left-4`} onClick={() => router.push('/')} />
|
||||||
|
<div className="mb-8 bg-gray-800/90 rounded-lg p-4">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
|
<h1 className='text-4xl font-bold text-white'>{processedEvent.name}</h1>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{processedEvent.topics && processedEvent.topics.length > 0 && (
|
||||||
|
processedEvent.topics.map((topic, index) => (
|
||||||
|
<Tag className='text-white' key={index} value={topic}></Tag>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='text-xl text-gray-200 mb-4 mt-4'>{processedEvent.description}</p>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<Image
|
||||||
|
alt="avatar image"
|
||||||
|
src={returnImageProxy(author?.avatar, author?.pubkey)}
|
||||||
|
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 || author?.name || author?.pubkey}
|
||||||
|
</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'>
|
||||||
|
{renderPaymentMessage()}
|
||||||
|
{processedEvent?.pubkey === session?.user?.pubkey && (
|
||||||
|
<div className='flex space-x-2 mt-4 sm:mt-0'>
|
||||||
|
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||||
|
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
117
src/components/content/courses/DocumentLesson.js
Normal file
117
src/components/content/courses/DocumentLesson.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect, useState } 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 { getTotalFromZaps } from "@/utils/lightning";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const MDDisplay = dynamic(
|
||||||
|
() => import("@uiw/react-markdown-preview"),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
||||||
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
|
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!zaps || zapsLoading || zapsError) return;
|
||||||
|
const total = getTotalFromZaps(zaps, lesson);
|
||||||
|
setZapAmount(total);
|
||||||
|
}, [zaps, zapsLoading, zapsError, lesson]);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isPaid && decryptionPerformed) {
|
||||||
|
return <MDDisplay className='p-4 rounded-lg w-full' source={lesson.content} />;
|
||||||
|
}
|
||||||
|
if (isPaid && !decryptionPerformed) {
|
||||||
|
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 <MDDisplay className='p-4 rounded-lg w-full' source={lesson.content} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<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="w-full mx-auto px-4 py-8 -mt-32 relative z-10">
|
||||||
|
<div className="mb-8 bg-gray-800/90 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>
|
||||||
|
<p className='text-xl text-gray-200 mb-4 mt-4'>{lesson.summary}</p>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
{renderContent()}
|
||||||
|
{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentLesson;
|
130
src/components/content/courses/VideoLesson.js
Normal file
130
src/components/content/courses/VideoLesson.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import React, { useEffect, useState } 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 { getTotalFromZaps } from "@/utils/lightning";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const MDDisplay = dynamic(
|
||||||
|
() => import("@uiw/react-markdown-preview"),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
||||||
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
|
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!zaps || zapsLoading || zapsError) return;
|
||||||
|
const total = getTotalFromZaps(zaps, lesson);
|
||||||
|
setZapAmount(total);
|
||||||
|
}, [zaps, zapsLoading, zapsError, lesson]);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isPaid && decryptionPerformed) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full aspect-video rounded-lg mb-4">
|
||||||
|
{/* Add your video player component here */}
|
||||||
|
<video controls className="w-full h-full">
|
||||||
|
<source src={lesson.videoUrl} type="video/mp4" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<MDDisplay className='p-4 rounded-lg w-full' source={lesson.content} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isPaid && !decryptionPerformed) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (lesson?.content) {
|
||||||
|
return <MDDisplay className='p-4 rounded-lg w-full' source={lesson.content} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{renderContent()}
|
||||||
|
<div className="bg-gray-800/90 rounded-lg p-4 m-4">
|
||||||
|
<div className="w-full flex flex-col items-start justify-start mt-2 px-2">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
|
<h1 className='text-3xl text-white'>{lesson.title}</h1>
|
||||||
|
{lesson.topics && lesson.topics.length > 0 && (
|
||||||
|
lesson.topics.map((topic, index) => (
|
||||||
|
<Tag className='mt-2 text-white' key={index} value={topic}></Tag>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row items-center justify-between w-full'>
|
||||||
|
<p className='text-xl mt-4 text-gray-200'>{lesson.summary}</p>
|
||||||
|
<ZapDisplay
|
||||||
|
zapAmount={zapAmount}
|
||||||
|
event={lesson}
|
||||||
|
zapsLoading={zapsLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-full flex flex-col space-y-4 mt-4'>
|
||||||
|
<div className='flex flex-row justify-between items-center'>
|
||||||
|
<div className='flex flex-row w-fit 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'>
|
||||||
|
Created 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{lesson?.additionalLinks && lesson.additionalLinks.length > 0 && (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoLesson;
|
171
src/components/content/documents/DocumentDetails.js
Normal file
171
src/components/content/documents/DocumentDetails.js
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
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 lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
|
||||||
|
|
||||||
|
const DocumentDetails = ({ processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, handlePaymentSuccess, handlePaymentError, authorView }) => {
|
||||||
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
|
const router = useRouter();
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const windowWidth = useWindowWidth();
|
||||||
|
|
||||||
|
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 && 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPaymentMessage = () => {
|
||||||
|
if (session?.user && 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 (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-4 rounded-lg w-full' source={decryptedContent} />;
|
||||||
|
}
|
||||||
|
if (paidResource && !decryptedContent) {
|
||||||
|
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 className="flex flex-row items-center justify-center w-full mt-4">
|
||||||
|
<ResourcePaymentButton
|
||||||
|
lnAddress={lnAddress}
|
||||||
|
amount={price}
|
||||||
|
onSuccess={handlePaymentSuccess}
|
||||||
|
onError={handlePaymentError}
|
||||||
|
resourceId={processedEvent.d}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (processedEvent?.content) {
|
||||||
|
return <MDDisplay className='p-4 rounded-lg w-full' source={processedEvent.content} />;
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
<div className="mb-8 bg-gray-800/90 rounded-lg p-4">
|
||||||
|
<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 && topics.length > 0 && (
|
||||||
|
topics.map((topic, index) => (
|
||||||
|
<Tag className='text-white' key={index} value={topic}></Tag>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='text-xl text-gray-200 mb-4 mt-4'>{summary}</p>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
<div className='w-full mt-8 flex flex-wrap justify-between items-center'>
|
||||||
|
{renderPaymentMessage()}
|
||||||
|
{authorView && (
|
||||||
|
<div className='flex space-x-2 mt-4 sm:mt-0'>
|
||||||
|
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||||
|
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentDetails;
|
@ -1,112 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
|
|
||||||
|
|
||||||
const ResourceDetails = ({processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, handlePaymentSuccess, handlePaymentError}) => {
|
|
||||||
const [zapAmount, setZapAmount] = useState(0);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { returnImageProxy } = useImageProxy();
|
|
||||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
|
|
||||||
const { data: session, status } = useSession();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (zaps.length > 0) {
|
|
||||||
const total = getTotalFromZaps(zaps, processedEvent);
|
|
||||||
setZapAmount(total);
|
|
||||||
}
|
|
||||||
}, [zaps, processedEvent]);
|
|
||||||
|
|
||||||
const renderPaymentMessage = () => {
|
|
||||||
if (session?.user && 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 (paidResource && decryptedContent && author && processedEvent?.pubkey !== session?.user?.pubkey && !session?.user?.role?.subscribed) {
|
|
||||||
return <GenericButton tooltipOptions={{position: 'top'}} tooltip={`Pay ${processedEvent.price} sats to access this content or subscribe to get access to all content`} 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col'>
|
|
||||||
<i className='pi pi-arrow-left pr-8 cursor-pointer hover:opacity-75 max-tab:pl-2 max-tab:my-4' onClick={() => router.push('/')} />
|
|
||||||
<div className='w-[75vw] mx-auto flex flex-row items-start justify-between max-tab:flex-col max-mob:flex-col max-tab:w-[100vw] max-mob:w-[100vw] max-tab:px-2 max-mob:px-2'>
|
|
||||||
<div className='flex flex-col items-start max-w-[45vw] max-tab:max-w-[100vw] max-mob:max-w-[100vw]'>
|
|
||||||
<div className='pt-2 flex flex-row justify-start w-full'>
|
|
||||||
{topics && topics.length > 0 && (
|
|
||||||
topics.map((topic, index) => (
|
|
||||||
<Tag className='mr-2 text-white' key={index} value={topic}></Tag>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<h1 className='text-4xl mt-6'>{title}</h1>
|
|
||||||
<p className='text-xl mt-6'>{summary}</p>
|
|
||||||
<div className='flex flex-row w-full mt-6 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'>
|
|
||||||
Created by{' '}
|
|
||||||
<a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
|
|
||||||
{author?.username}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
|
|
||||||
{image && (
|
|
||||||
<div className='flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md max-tab:w-full max-tab:mx-auto max-tab:h-auto'>
|
|
||||||
<Image
|
|
||||||
alt="resource thumbnail"
|
|
||||||
src={returnImageProxy(image)}
|
|
||||||
width={344}
|
|
||||||
height={194}
|
|
||||||
className="w-[344px] h-[194px] object-cover object-top rounded-lg max-tab:w-full max-tab:h-auto"
|
|
||||||
/>
|
|
||||||
<div className='w-full flex flex-row justify-between'>
|
|
||||||
{paidResource && !decryptedContent && <ResourcePaymentButton
|
|
||||||
lnAddress={lnAddress}
|
|
||||||
amount={price}
|
|
||||||
onSuccess={handlePaymentSuccess}
|
|
||||||
onError={handlePaymentError}
|
|
||||||
resourceId={processedEvent.d}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
{renderPaymentMessage()}
|
|
||||||
|
|
||||||
<ZapDisplay
|
|
||||||
zapAmount={zapAmount}
|
|
||||||
event={processedEvent}
|
|
||||||
zapsLoading={zapsLoading && zapAmount === 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResourceDetails;
|
|
173
src/components/content/videos/VideoDetails.js
Normal file
173
src/components/content/videos/VideoDetails.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
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 lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
|
||||||
|
|
||||||
|
const VideoDetails = ({ processedEvent, topics, title, summary, image, price, author, paidResource, decryptedContent, handlePaymentSuccess, handlePaymentError, authorView }) => {
|
||||||
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
|
const router = useRouter();
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const windowWidth = useWindowWidth();
|
||||||
|
|
||||||
|
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 && 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPaymentMessage = () => {
|
||||||
|
if (session?.user && 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 (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-4 rounded-lg w-full' source={decryptedContent} />;
|
||||||
|
}
|
||||||
|
if (paidResource && !decryptedContent) {
|
||||||
|
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(${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 className="flex flex-row items-center justify-center w-full mt-4 z-10">
|
||||||
|
<ResourcePaymentButton
|
||||||
|
lnAddress={lnAddress}
|
||||||
|
amount={price}
|
||||||
|
onSuccess={handlePaymentSuccess}
|
||||||
|
onError={handlePaymentError}
|
||||||
|
resourceId={processedEvent.d}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (processedEvent?.content) {
|
||||||
|
return <MDDisplay className='p-4 rounded-lg w-full' source={processedEvent.content} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{renderContent()}
|
||||||
|
<div className="bg-gray-800/90 rounded-lg p-4 m-4">
|
||||||
|
<div className="w-full flex flex-col items-start justify-start mt-2 px-2">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
|
<h1 className='text-4xl'>{title}</h1>
|
||||||
|
{topics && topics.length > 0 && (
|
||||||
|
topics.map((topic, index) => (
|
||||||
|
<Tag className='mt-2 text-white' key={index} value={topic}></Tag>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row items-center justify-between w-full'>
|
||||||
|
<p className='text-xl mt-4'>{summary}</p>
|
||||||
|
<ZapDisplay
|
||||||
|
zapAmount={zapAmount}
|
||||||
|
event={processedEvent}
|
||||||
|
zapsLoading={zapsLoading && zapAmount === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-full flex flex-col space-y-4 mt-4'>
|
||||||
|
<div className='flex flex-row justify-between items-center'>
|
||||||
|
<div className='flex flex-row w-fit 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'>
|
||||||
|
Created by{' '}
|
||||||
|
<a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
|
||||||
|
{author?.username}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{authorView && (
|
||||||
|
<div className='flex flex-row justify-center items-center space-x-2'>
|
||||||
|
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||||
|
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='w-full flex flex-row justify-between'>
|
||||||
|
{renderPaymentMessage()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoDetails;
|
@ -32,7 +32,7 @@ const Navbar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-[100vw] h-fit'>
|
<div className='w-[100vw] h-fit z-20'>
|
||||||
<Menubar
|
<Menubar
|
||||||
start={start}
|
start={start}
|
||||||
end={UserAvatar}
|
end={UserAvatar}
|
||||||
|
@ -49,7 +49,7 @@ export default function MyApp({
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className='max-w-[100vw] pl-[14vw] max-sidebar:pl-0 pb-16 max-sidebar:pb-20'>
|
<div className='w-[100vw] pl-[14vw] max-sidebar:pl-0 pb-16 max-sidebar:pb-20'>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,6 +80,8 @@ const ContentPage = () => {
|
|||||||
const [selectedTopic, setSelectedTopic] = useState('All')
|
const [selectedTopic, setSelectedTopic] = useState('All')
|
||||||
const [filteredContent, setFilteredContent] = useState([]);
|
const [filteredContent, setFilteredContent] = useState([]);
|
||||||
|
|
||||||
|
const memoizedFilteredContent = useMemo(() => filteredContent, [filteredContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tag = router.query.tag;
|
const tag = router.query.tag;
|
||||||
if (tag) {
|
if (tag) {
|
||||||
@ -150,8 +152,8 @@ const ContentPage = () => {
|
|||||||
const renderCarousels = () => {
|
const renderCarousels = () => {
|
||||||
return (
|
return (
|
||||||
<GenericCarousel
|
<GenericCarousel
|
||||||
key={selectedTopic} // Add this line
|
key={`${selectedTopic}-${memoizedFilteredContent.length}`}
|
||||||
items={filteredContent}
|
items={memoizedFilteredContent}
|
||||||
selectedTopic={selectedTopic}
|
selectedTopic={selectedTopic}
|
||||||
title={`${selectedTopic} Content`}
|
title={`${selectedTopic} Content`}
|
||||||
type="all"
|
type="all"
|
||||||
|
@ -2,7 +2,10 @@ import React, { useEffect, useState, useCallback } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { parseCourseEvent, parseEvent, findKind0Fields } from "@/utils/nostr";
|
import { parseCourseEvent, parseEvent, findKind0Fields } from "@/utils/nostr";
|
||||||
import CourseDetails from "@/components/content/courses/CourseDetails";
|
import CourseDetails from "@/components/content/courses/CourseDetails";
|
||||||
import CourseLesson from "@/components/content/courses/CourseLesson";
|
import VideoLesson from "@/components/content/courses/VideoLesson";
|
||||||
|
import DocumentLesson from "@/components/content/courses/DocumentLesson";
|
||||||
|
import CourseDetailsNew from "@/components/content/courses/CourseDetailsNew";
|
||||||
|
import { Divider } from "primereact/divider";
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useNDKContext } from "@/context/NDKContext";
|
import { useNDKContext } from "@/context/NDKContext";
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
@ -181,7 +184,7 @@ const Course = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CourseDetails
|
<CourseDetailsNew
|
||||||
processedEvent={course}
|
processedEvent={course}
|
||||||
paidCourse={paidCourse}
|
paidCourse={paidCourse}
|
||||||
lessons={lessons}
|
lessons={lessons}
|
||||||
@ -190,7 +193,11 @@ const Course = () => {
|
|||||||
handlePaymentError={handlePaymentError}
|
handlePaymentError={handlePaymentError}
|
||||||
/>
|
/>
|
||||||
{lessons.length > 0 && lessons.map((lesson, index) => (
|
{lessons.length > 0 && lessons.map((lesson, index) => (
|
||||||
<CourseLesson key={index} lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} />
|
<div key={index} className="w-full p-4">
|
||||||
|
<h1 className="text-2xl font-bold text-white">Lesson {index + 1}</h1>
|
||||||
|
<Divider />
|
||||||
|
{lesson.type === 'workshop' ? <VideoLesson key={index} lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} /> : <DocumentLesson key={index} lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} />}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="mx-auto my-6">
|
<div className="mx-auto my-6">
|
||||||
{course?.content && <MDDisplay className='p-4 rounded-lg' source={course.content} />}
|
{course?.content && <MDDisplay className='p-4 rounded-lg' source={course.content} />}
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
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 } from '@/utils/nostr';
|
import { parseEvent, findKind0Fields } from '@/utils/nostr';
|
||||||
import GenericButton from '@/components/buttons/GenericButton';
|
|
||||||
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 dynamic from 'next/dynamic';
|
|
||||||
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useNDKContext } from '@/context/NDKContext';
|
import { useNDKContext } from '@/context/NDKContext';
|
||||||
import ResourceDetails from '@/components/content/resources/ResourceDetails';
|
import VideoDetails from '@/components/content/videos/VideoDetails';
|
||||||
|
import DocumentDetails from '@/components/content/documents/DocumentDetails';
|
||||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
const MDDisplay = dynamic(
|
|
||||||
() => import("@uiw/react-markdown-preview"),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -55,22 +46,21 @@ export default function Details() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const decryptContent = async () => {
|
const decryptContent = async () => {
|
||||||
if (user && paidResource) {
|
if (paidResource && processedEvent.content) {
|
||||||
if (user?.purchased?.length > 0) {
|
// Check if user is subscribed first
|
||||||
const purchasedResource = user?.purchased.find(purchase => purchase.resourceId === processedEvent.d);
|
if (user?.role?.subscribed) {
|
||||||
if (purchasedResource) {
|
|
||||||
console.log("purchasedResource", purchasedResource)
|
|
||||||
const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content);
|
|
||||||
setDecryptedContent(decryptedContent);
|
|
||||||
}
|
|
||||||
} else if (user?.role && user?.role.subscribed) {
|
|
||||||
// decrypt the content
|
|
||||||
const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content);
|
const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content);
|
||||||
setDecryptedContent(decryptedContent);
|
setDecryptedContent(decryptedContent);
|
||||||
}
|
}
|
||||||
|
// If not subscribed, check if they have purchased
|
||||||
|
else if (user?.purchased?.some(purchase => purchase.resourceId === processedEvent.d)) {
|
||||||
|
const decryptedContent = await nip04.decrypt(privkey, pubkey, processedEvent.content);
|
||||||
|
setDecryptedContent(decryptedContent);
|
||||||
|
}
|
||||||
|
// If neither subscribed nor purchased, decryptedContent remains null
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
|
||||||
decryptContent();
|
decryptContent();
|
||||||
}, [user, paidResource, processedEvent]);
|
}, [user, paidResource, processedEvent]);
|
||||||
|
|
||||||
@ -156,6 +146,7 @@ export default function Details() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (event) {
|
if (event) {
|
||||||
const parsedEvent = parseEvent(event);
|
const parsedEvent = parseEvent(event);
|
||||||
|
console.log("parsedEvent", parsedEvent);
|
||||||
setProcessedEvent(parsedEvent);
|
setProcessedEvent(parsedEvent);
|
||||||
}
|
}
|
||||||
}, [event]);
|
}, [event]);
|
||||||
@ -171,25 +162,6 @@ export default function Details() {
|
|||||||
}
|
}
|
||||||
}, [processedEvent]);
|
}, [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 && 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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePaymentSuccess = async (response, newResource) => {
|
const handlePaymentSuccess = async (response, newResource) => {
|
||||||
if (response && response?.preimage) {
|
if (response && response?.preimage) {
|
||||||
console.log("newResource", newResource);
|
console.log("newResource", newResource);
|
||||||
@ -204,19 +176,6 @@ export default function Details() {
|
|||||||
showToast('error', 'Payment Error', `Failed to purchase resource. Please try again. Error: ${error}`);
|
showToast('error', 'Payment Error', `Failed to purchase resource. Please try again. Error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
if (decryptedContent) {
|
|
||||||
return <MDDisplay className='p-4 rounded-lg w-full' source={decryptedContent} />;
|
|
||||||
}
|
|
||||||
if (paidResource && !decryptedContent) {
|
|
||||||
return <p className="text-center text-xl text-red-500">This content is paid and needs to be purchased before viewing.</p>;
|
|
||||||
}
|
|
||||||
if (processedEvent?.content) {
|
|
||||||
return <MDDisplay className='p-4 rounded-lg w-full' source={processedEvent.content} />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="mx-auto">
|
return <div className="mx-auto">
|
||||||
<ProgressSpinner />
|
<ProgressSpinner />
|
||||||
@ -230,9 +189,9 @@ export default function Details() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
{processedEvent && (
|
{processedEvent && processedEvent.type !== "workshop" ? (
|
||||||
<ResourceDetails
|
<DocumentDetails
|
||||||
processedEvent={processedEvent}
|
processedEvent={processedEvent}
|
||||||
topics={processedEvent.topics}
|
topics={processedEvent.topics}
|
||||||
title={processedEvent.title}
|
title={processedEvent.title}
|
||||||
@ -244,31 +203,34 @@ export default function Details() {
|
|||||||
decryptedContent={decryptedContent}
|
decryptedContent={decryptedContent}
|
||||||
handlePaymentSuccess={handlePaymentSuccess}
|
handlePaymentSuccess={handlePaymentSuccess}
|
||||||
handlePaymentError={handlePaymentError}
|
handlePaymentError={handlePaymentError}
|
||||||
|
authorView={authorView}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<VideoDetails
|
||||||
|
processedEvent={processedEvent}
|
||||||
|
topics={processedEvent.topics}
|
||||||
|
title={processedEvent.title}
|
||||||
|
summary={processedEvent.summary}
|
||||||
|
image={processedEvent.image}
|
||||||
|
price={processedEvent.price}
|
||||||
|
author={author}
|
||||||
|
paidResource={paidResource}
|
||||||
|
decryptedContent={decryptedContent}
|
||||||
|
handlePaymentSuccess={handlePaymentSuccess}
|
||||||
|
handlePaymentError={handlePaymentError}
|
||||||
|
authorView={authorView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{authorView && (
|
|
||||||
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
|
|
||||||
<div className='w-fit flex flex-row justify-between'>
|
|
||||||
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
|
|
||||||
<GenericButton 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 max-tab:px-4'>
|
<div className='max-tab:px-4'>
|
||||||
<ZapThreadsWrapper
|
<ZapThreadsWrapper
|
||||||
anchor={nAddress}
|
anchor={nAddress}
|
||||||
user={user?.pubkey || null}
|
user={user?.pubkey || null}
|
||||||
relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.mutinywallet.com/, wss://relay.primal.net/"
|
relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.mutinywallet.com/, wss://relay.primal.net/"
|
||||||
disable=""
|
disable="zaps"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:w-[100vw] max-mob:w-[100vw]'>
|
|
||||||
{
|
|
||||||
processedEvent && processedEvent.content && renderContent()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user