2025-04-17 13:00:58 -05:00
import React , { useEffect , useState , useRef } 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' ;
import { Toast } from 'primereact/toast' ;
import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu' ;
2025-04-17 14:45:16 -05:00
import React , { useEffect , useState , useRef } from 'react' ;
// Needed for nip19 encoding on the client
import { Buffer } from 'buffer' ;
2025-04-17 13:00:58 -05:00
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper' ;
import appConfig from '@/config/appConfig' ;
import { nip19 } from 'nostr-tools' ;
const MDDisplay = dynamic ( ( ) => import ( '@uiw/react-markdown-preview' ) , {
2025-04-02 17:47:30 -05:00
ssr : false ,
} ) ;
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
const VideoDetails = ( {
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 ( ) ;
2025-04-02 16:38:37 -05:00
const { zaps , zapsLoading , zapsError } = useZapsSubscription ( {
event : processedEvent ,
} ) ;
2025-04-02 17:47:30 -05:00
const { data : session , status } = useSession ( ) ;
const { showToast } = useToast ( ) ;
const windowWidth = useWindowWidth ( ) ;
const isMobileView = windowWidth <= 768 ;
const menuRef = useRef ( null ) ;
const toastRef = useRef ( null ) ;
2025-04-02 16:38:37 -05:00
const [ nsec , setNsec ] = useState ( null ) ;
const [ npub , setNpub ] = useState ( null ) ;
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
const handleDelete = async ( ) => {
try {
const response = await axios . delete ( ` /api/resources/ ${ processedEvent . d } ` ) ;
if ( response . status === 204 ) {
2025-04-17 13:00:58 -05:00
showToast ( 'success' , 'Success' , 'Resource deleted successfully.' ) ;
router . push ( '/' ) ;
2025-04-02 17:47:30 -05:00
}
} catch ( error ) {
if (
error . response &&
error . response . data &&
2025-04-17 13:00:58 -05:00
error . response . data . error . includes ( 'Invalid `prisma.resource.delete()`' )
2025-04-02 17:47:30 -05:00
) {
showToast (
2025-04-17 13:00:58 -05:00
'error' ,
'Error' ,
'Resource cannot be deleted because it is part of a course, delete the course first.'
2025-04-02 17:47:30 -05:00
) ;
2025-04-17 13:00:58 -05:00
} else if ( error . response && error . response . data && error . response . data . error ) {
showToast ( 'error' , 'Error' , error . response . data . error ) ;
2025-04-02 17:47:30 -05:00
} else {
2025-04-17 13:00:58 -05:00
showToast ( 'error' , 'Error' , 'Failed to delete resource. Please try again.' ) ;
2025-04-02 17:47:30 -05:00
}
2024-09-12 17:39:47 -05:00
}
2025-04-02 17:47:30 -05:00
} ;
const authorMenuItems = [
{
2025-04-17 13:00:58 -05:00
label : 'Edit' ,
icon : 'pi pi-pencil' ,
2025-04-02 17:47:30 -05:00
command : ( ) => router . push ( ` /details/ ${ processedEvent . id } /edit ` ) ,
} ,
{
2025-04-17 13:00:58 -05:00
label : 'Delete' ,
icon : 'pi pi-trash' ,
2025-04-02 17:47:30 -05:00
command : handleDelete ,
} ,
{
2025-04-17 13:00:58 -05:00
label : 'View Nostr note' ,
icon : 'pi pi-globe' ,
2025-04-02 17:47:30 -05:00
command : ( ) => {
2025-04-17 13:00:58 -05:00
window . open ( ` https://habla.news/a/ ${ nAddress } ` , '_blank' ) ;
2025-04-02 17:47:30 -05:00
} ,
} ,
] ;
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
const userMenuItems = [
{
2025-04-17 13:00:58 -05:00
label : 'View Nostr note' ,
icon : 'pi pi-globe' ,
2025-04-02 17:47:30 -05:00
command : ( ) => {
2025-04-17 13:00:58 -05:00
window . open ( ` https://habla.news/a/ ${ nAddress } ` , '_blank' ) ;
2025-04-02 17:47:30 -05:00
} ,
} ,
] ;
2025-03-30 17:31:53 -05:00
2025-04-02 17:47:30 -05:00
if ( course ) {
userMenuItems . unshift ( {
2025-04-17 13:00:58 -05:00
label : isMobileView ? 'Course' : 'Open Course' ,
icon : 'pi pi-external-link' ,
command : ( ) => window . open ( ` /course/ ${ course } ` , '_blank' ) ,
2025-04-02 17:47:30 -05:00
} ) ;
}
2025-03-30 17:31:53 -05:00
2025-04-02 17:47:30 -05:00
useEffect ( ( ) => {
if ( isLesson ) {
axios
. get ( ` /api/resources/ ${ processedEvent . d } ` )
2025-04-17 13:00:58 -05:00
. then ( res => {
2025-04-02 17:47:30 -05:00
if ( res . data && res . data . lessons [ 0 ] ? . courseId ) {
setCourse ( res . data . lessons [ 0 ] ? . courseId ) ;
}
} )
2025-04-17 13:00:58 -05:00
. catch ( err => {
console . error ( 'err' , err ) ;
2025-03-30 17:31:53 -05:00
} ) ;
}
2025-04-02 17:47:30 -05:00
} , [ processedEvent . d , isLesson ] ) ;
2025-03-30 17:31:53 -05:00
2025-04-02 17:47:30 -05:00
useEffect ( ( ) => {
if ( zaps . length > 0 ) {
const total = getTotalFromZaps ( zaps , processedEvent ) ;
setZapAmount ( total ) ;
}
} , [ zaps , processedEvent ] ) ;
2025-03-30 17:31:53 -05:00
2025-04-02 16:38:37 -05:00
useEffect ( ( ) => {
if ( session ? . user ? . privkey ) {
2025-04-17 13:00:58 -05:00
const privkeyBuffer = Buffer . from ( session . user . privkey , 'hex' ) ;
2025-04-02 16:38:37 -05:00
setNsec ( nip19 . nsecEncode ( privkeyBuffer ) ) ;
} else if ( session ? . user ? . pubkey ) {
setNpub ( nip19 . npubEncode ( session . user . pubkey ) ) ;
}
} , [ session ] ) ;
2025-04-02 17:47:30 -05:00
const renderPaymentMessage = ( ) => {
if ( session ? . user && session . user ? . role ? . subscribed && decryptedContent ) {
return (
< GenericButton
2025-04-17 13:00:58 -05:00
tooltipOptions = { { position : 'top' } }
2025-04-02 17:47:30 -05:00
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 the user paid for the course that this lesson is in, show a message that says you have this lesson through the course and show how much you paid for the course
if (
isLesson &&
course &&
2025-04-17 13:00:58 -05:00
session ? . user ? . purchased ? . some ( purchase => purchase . courseId === course )
2025-04-02 17:47:30 -05:00
) {
return (
< GenericButton
2025-04-17 13:00:58 -05:00
tooltipOptions = { { position : 'top' } }
2025-04-02 16:38:37 -05:00
tooltip = { ` You have this lesson through purchasing the course it belongs to. You paid ${
2025-04-17 13:00:58 -05:00
session ? . user ? . purchased ? . find ( purchase => purchase . courseId === course ) ? . course ? . price
2025-04-02 16:38:37 -05:00
} sats for the course . ` }
2025-04-02 17:47:30 -05:00
icon = "pi pi-check"
2025-04-02 16:38:37 -05:00
label = { ` Paid ${
2025-04-17 13:00:58 -05:00
session ? . user ? . purchased ? . find ( purchase => purchase . courseId === course ) ? . course ? . price
2025-04-02 16:38:37 -05:00
} sats ` }
2025-04-02 17:47:30 -05:00
severity = "success"
outlined
size = "small"
className = "cursor-default hover:opacity-100 hover:bg-transparent focus:ring-0"
/ >
) ;
}
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
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"
/ >
) ;
}
2024-09-12 17:39:47 -05:00
2025-04-17 13:00:58 -05:00
if ( paidResource && author && processedEvent ? . pubkey === session ? . user ? . pubkey ) {
2025-04-02 17:47:30 -05:00
return (
< GenericButton
2025-04-17 13:00:58 -05:00
tooltipOptions = { { position : 'top' } }
2025-04-02 17:47:30 -05:00
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"
/ >
) ;
}
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
return null ;
} ;
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
const renderContent = ( ) => {
if ( decryptedContent ) {
2025-04-17 13:00:58 -05:00
return < MDDisplay className = "p-0 rounded-lg w-full" source = { decryptedContent } / > ;
2025-04-02 17:47:30 -05:00
}
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 } ) ` ,
2025-04-17 13:00:58 -05:00
backgroundSize : 'cover' ,
backgroundPosition : 'center' ,
2025-04-02 17:47:30 -05:00
} }
> < / d i v >
< div className = "absolute inset-0 bg-black bg-opacity-50" > < / d i v >
< div className = "mx-auto py-auto z-10" >
< i className = "pi pi-lock text-[100px] text-red-500" > < / i >
< / d i v >
< 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 = { author ? . lud16 }
amount = { price }
onSuccess = { handlePaymentSuccess }
onError = { handlePaymentError }
resourceId = { processedEvent . d }
/ >
< / d i v >
< / d i v >
) ;
2024-09-12 17:39:47 -05:00
}
2025-04-02 17:47:30 -05:00
if ( processedEvent ? . content ) {
2025-04-17 13:00:58 -05:00
return < MDDisplay className = "p-0 rounded-lg w-full" source = { processedEvent . content } / > ;
2025-04-02 17:47:30 -05:00
}
return null ;
} ;
2024-09-12 17:39:47 -05:00
2025-04-02 17:47:30 -05:00
return (
< div className = "w-full" >
< Toast ref = { toastRef } / >
{ renderContent ( ) }
< div className = "bg-gray-800/90 rounded-lg p-4 m-4 max-mob:m-0 max-tab:m-0 max-mob:rounded-t-none max-tab:rounded-t-none" >
2025-04-17 13:00:58 -05:00
< div className = { ` w-full flex flex-col items-start justify-start mt-2 px-2 ` } >
2025-04-02 17:47:30 -05:00
< div className = "flex flex-col items-start gap-2 w-full" >
< div className = "flex flex-row items-center justify-between gap-2 w-full" >
< h1 className = "text-4xl flex-grow" > { title } < / h 1 >
< ZapDisplay
zapAmount = { zapAmount }
event = { processedEvent }
zapsLoading = { zapsLoading && zapAmount === 0 }
/ >
< / d i v >
< div className = "flex flex-row items-center gap-2 w-full" >
{ topics &&
topics . length > 0 &&
topics . map ( ( topic , index ) => (
2025-04-17 13:00:58 -05:00
< Tag className = "mt-2 text-white" key = { index } value = { topic } > < / T a g >
2025-04-02 17:47:30 -05:00
) ) }
{ isLesson && < Tag className = "mt-2 text-white" value = "lesson" / > }
< / d i v >
< / d i v >
< div className = "flex flex-row items-center justify-between w-full" >
< div className = "my-4 max-mob:text-base max-tab:text-base" >
2025-04-17 13:00:58 -05:00
{ summary ? . split ( '\n' ) . map ( ( line , index ) => (
2025-04-02 17:47:30 -05:00
< p key = { index } > { line } < / p >
) ) }
2024-09-12 17:39:47 -05:00
< / d i v >
2025-04-02 17:47:30 -05:00
< / d i v >
< div className = "flex items-center justify-between w-full" >
< 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" >
2025-04-17 13:00:58 -05:00
By { ' ' }
2025-04-02 17:47:30 -05:00
< a
rel = "noreferrer noopener"
target = "_blank"
className = "text-blue-300 hover:underline"
>
{ author ? . username }
< / a >
< / p >
< / d i v >
< div className = "flex justify-end" >
< MoreOptionsMenu
menuItems = { authorView ? authorMenuItems : userMenuItems }
additionalLinks = { processedEvent ? . additionalLinks || [ ] }
isMobileView = { isMobileView }
/ >
< / d i v >
< / d i v >
2024-09-12 17:39:47 -05:00
< / d i v >
2025-04-17 13:00:58 -05:00
< div className = "w-full flex justify-start mt-4" > { renderPaymentMessage ( ) } < / d i v >
2025-04-02 16:38:37 -05:00
{ nAddress && (
< div className = "mt-8" >
2025-04-17 13:00:58 -05:00
{ ! paidResource || decryptedContent || session ? . user ? . role ? . subscribed ? (
2025-04-02 16:38:37 -05:00
< ZapThreadsWrapper
anchor = { nAddress }
user = { session ? . user ? nsec || npub : null }
2025-04-17 13:00:58 -05:00
relays = { appConfig . defaultRelayUrls . join ( ',' ) }
2025-04-02 16:38:37 -05:00
disable = "zaps"
isAuthorized = { true }
/ >
) : (
< div className = "text-center p-4 bg-gray-800/50 rounded-lg" >
< p className = "text-gray-400" >
2025-04-17 13:00:58 -05:00
Comments are only available to content purchasers , subscribers , and the content
creator .
2025-04-02 16:38:37 -05:00
< / p >
< / d i v >
) }
< / d i v >
) }
2025-04-02 17:47:30 -05:00
< / d i v >
< / d i v >
) ;
} ;
2024-09-12 17:39:47 -05:00
2024-10-16 16:54:12 -05:00
export default VideoDetails ;