From ed41f9a170ff83446cc45c6589ffd183b42f993d Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Sun, 30 Mar 2025 17:31:53 -0500 Subject: [PATCH] Standardize details layout on contet, consolidate options into new generic moreOptionsMenu component --- .../content/combined/CombinedDetails.js | 182 ++++++++--------- .../content/courses/CombinedLesson.js | 104 ++++------ .../content/courses/CourseDetails.js | 129 ++++++------ .../content/courses/CourseLesson.js | 141 +++++++------ .../content/courses/DocumentLesson.js | 105 ++++------ src/components/content/courses/VideoLesson.js | 127 +++++------- .../content/documents/DocumentDetails.js | 179 ++++++++--------- src/components/content/videos/VideoDetails.js | 190 ++++++++---------- src/components/ui/MoreOptionsMenu.js | 69 +++++++ 9 files changed, 578 insertions(+), 648 deletions(-) create mode 100644 src/components/ui/MoreOptionsMenu.js diff --git a/src/components/content/combined/CombinedDetails.js b/src/components/content/combined/CombinedDetails.js index f28124f..18f8d0e 100644 --- a/src/components/content/combined/CombinedDetails.js +++ b/src/components/content/combined/CombinedDetails.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import axios from "axios"; import { useToast } from "@/hooks/useToast"; import { Tag } from "primereact/tag"; @@ -13,6 +13,8 @@ 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"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -29,6 +31,62 @@ const CombinedDetails = ({ processedEvent, topics, title, summary, image, price, const { showToast } = useToast(); const windowWidth = useWindowWidth(); const isMobileView = windowWidth <= 768; + const menuRef = useRef(null); + const toastRef = useRef(null); + + 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 authorMenuItems = [ + { + label: 'Edit', + icon: 'pi pi-pencil', + command: () => router.push(`/details/${processedEvent.id}/edit`) + }, + { + label: 'Delete', + icon: 'pi pi-trash', + command: handleDelete + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } + } + ]; + + const userMenuItems = [ + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } + } + ]; + + if (course) { + userMenuItems.unshift({ + label: isMobileView ? 'Course' : 'Open Course', + icon: 'pi pi-external-link', + command: () => window.open(`/course/${course}`, '_blank') + }); + } useEffect(() => { if (isLesson) { @@ -49,22 +107,6 @@ const CombinedDetails = ({ processedEvent, topics, title, summary, image, price, } }, [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 ; @@ -124,37 +166,9 @@ const CombinedDetails = ({ processedEvent, topics, title, summary, image, price, return null; }; - const renderAdditionalLinks = () => { - if (processedEvent?.additionalLinks?.length > 0) { - return ( -
-

Additional Links:

- {processedEvent.additionalLinks.map((link, index) => ( -
- - {link} - -
- ))} -
- ); - } - return null; - }; - return (
+
background image

{title}

-
- {topics?.map((topic, index) => ( - - ))} - {isLesson && } -
+ +
+
+ {topics?.map((topic, index) => ( + + ))} + {isLesson && }
{summary?.split('\n').map((line, index) => (

{line}

))} - {renderAdditionalLinks()} -
+
avatar image

- +
+ +
-
- {authorView ? ( -
- {renderPaymentMessage()} -
- router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined /> - - window.open(`https://habla.news/a/${nAddress}`, '_blank')} - /> -
-
- ) : ( -
- {renderPaymentMessage()} -
- {course && ( - window.open(`/course/${course}`, '_blank')} - label={isMobileView ? "Course" : "Open Course"} - tooltip="This is a lesson in a course" - tooltipOptions={{ position: 'top' }} - /> - )} - window.open(`https://habla.news/a/${nAddress}`, '_blank')} - /> -
-
- )} +
+ {renderPaymentMessage()}
{renderContent()} diff --git a/src/components/content/courses/CombinedLesson.js b/src/components/content/courses/CombinedLesson.js index 13abd8e..202e993 100644 --- a/src/components/content/courses/CombinedLesson.js +++ b/src/components/content/courses/CombinedLesson.js @@ -4,7 +4,6 @@ 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"; @@ -14,6 +13,7 @@ import appConfig from "@/config/appConfig"; import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson'; import { Menu } from "primereact/menu"; import { Toast } from "primereact/toast"; +import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -69,6 +69,20 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple }); } } + }, + { + label: 'Open lesson', + icon: 'pi pi-arrow-up-right', + command: () => { + window.open(`/details/${lesson.id}`, '_blank'); + } + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } } ]; @@ -217,13 +231,18 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple

{lesson.title}

-
- {lesson.topics && lesson.topics.length > 0 && ( - lesson.topics.map((topic, index) => ( - - )) - )} -
+ +
+
+ {lesson.topics && lesson.topics.length > 0 && ( + lesson.topics.map((topic, index) => ( + + )) + )}
{lesson.summary && (
@@ -233,7 +252,7 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
)}
-
+
avatar image

- -
-
- { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> +
+ +
{!isVideo && } - {lesson?.additionalLinks && lesson.additionalLinks.length > 0 && ( -
-
-
-

External links:

- -
-
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
-
-
- )} - {!lesson?.additionalLinks || lesson.additionalLinks.length === 0 && ( -
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
- )} + {!isVideo && renderContent()}
- {!isVideo && renderContent()}
); }; diff --git a/src/components/content/courses/CourseDetails.js b/src/components/content/courses/CourseDetails.js index 82c9c7f..6c3dc91 100644 --- a/src/components/content/courses/CourseDetails.js +++ b/src/components/content/courses/CourseDetails.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import axios from 'axios'; import { useToast } from "@/hooks/useToast"; import { Tag } from 'primereact/tag'; @@ -19,6 +19,8 @@ import appConfig from "@/config/appConfig"; import useTrackCourse from '@/hooks/tracking/useTrackCourse'; import WelcomeModal from '@/components/onboarding/WelcomeModal'; import { ProgressSpinner } from 'primereact/progressspinner'; +import { Toast } from "primereact/toast"; +import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu"; export default function CourseDetails({ processedEvent, paidCourse, lessons, decryptionPerformed, handlePaymentSuccess, handlePaymentError }) { const [zapAmount, setZapAmount] = useState(0); @@ -32,6 +34,40 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec const windowWidth = useWindowWidth(); const isMobileView = windowWidth <= 768; const { ndk } = useNDKContext(); + const menuRef = useRef(null); + const toastRef = useRef(null); + + 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 menuItems = [ + { + label: processedEvent?.pubkey === session?.user?.pubkey ? 'Edit' : null, + icon: 'pi pi-pencil', + command: () => router.push(`/course/${processedEvent.d}/edit`), + visible: processedEvent?.pubkey === session?.user?.pubkey + }, + { + label: processedEvent?.pubkey === session?.user?.pubkey ? 'Delete' : null, + icon: 'pi pi-trash', + command: handleDelete, + visible: processedEvent?.pubkey === session?.user?.pubkey + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => window.open(`https://nostr.band/${nAddress}`, '_blank') + } + ]; const { isCompleted } = useTrackCourse({ courseId: processedEvent?.d, @@ -74,18 +110,6 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec } }, [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 @@ -114,42 +138,14 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec return null; }; - const renderAdditionalLinks = () => { - if (processedEvent?.additionalLinks && processedEvent.additionalLinks.length > 0) { - return ( -
-

Additional Links:

- {processedEvent.additionalLinks.map((link, index) => ( - - ))} -
- ); - } - return null; - }; - if (!processedEvent || !author) { return
; } return (
- + +
course image}

{processedEvent.name}

-
- {processedEvent.topics && processedEvent.topics.length > 0 && ( - processedEvent.topics.map((topic, index) => ( - - )) - )} -
+ +
+
+ {processedEvent.topics && processedEvent.topics.length > 0 && ( + processedEvent.topics.map((topic, index) => ( + + )) + )}
{processedEvent.description && ( processedEvent.description.split('\n').map((line, index) => ( @@ -179,7 +180,7 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec )) )}
-
+
avatar image

- +
+ +
-
+
{renderPaymentMessage()} - {processedEvent?.pubkey === session?.user?.pubkey ? ( -
- router.push(`/course/${processedEvent.d}/edit`)} label="Edit" severity='warning' outlined /> - - window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={isMobileView ? null : "View Nostr Event"} tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} /> -
- ) : ( -
- window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip={isMobileView ? null : "View Nostr Event"} tooltipOptions={{ position: paidCourse ? 'left' : 'right' }} /> -
- )} - {renderAdditionalLinks()}
diff --git a/src/components/content/courses/CourseLesson.js b/src/components/content/courses/CourseLesson.js index 11c7a23..983000d 100644 --- a/src/components/content/courses/CourseLesson.js +++ b/src/components/content/courses/CourseLesson.js @@ -6,11 +6,12 @@ import { getTotalFromZaps } from "@/utils/lightning"; import ZapDisplay from "@/components/zaps/ZapDisplay"; import dynamic from "next/dynamic"; import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; -import { Menu } from "primereact/menu"; import { Toast } from "primereact/toast"; -import GenericButton from "@/components/buttons/GenericButton"; import useTrackDocumentLesson from "@/hooks/tracking/useTrackDocumentLesson"; import useWindowWidth from "@/hooks/useWindowWidth"; +import { nip19 } from "nostr-tools"; +import appConfig from "@/config/appConfig"; +import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -62,9 +63,44 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete }); } } + }, + { + label: 'Open lesson', + icon: 'pi pi-arrow-up-right', + command: () => { + window.open(`/details/${lesson.id}`, '_blank'); + } + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + if (lesson?.d) { + const addr = nip19.naddrEncode({ + pubkey: lesson.pubkey, + kind: lesson.kind, + identifier: lesson.d, + relays: appConfig.defaultRelayUrls || [] + }); + window.open(`https://habla.news/a/${addr}`, '_blank'); + } + } } ]; + // Add additional links to menu items if they exist + if (lesson?.additionalLinks && lesson.additionalLinks.length > 0) { + lesson.additionalLinks.forEach((link, index) => { + menuItems.push({ + label: `Link: ${new URL(link).hostname}`, + icon: 'pi pi-external-link', + command: () => { + window.open(link, '_blank'); + } + }); + }); + } + useEffect(() => { if (!zaps || zapsLoading || zapsError) return; @@ -98,78 +134,52 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete
-
+
+

{lesson?.title}

+ +
+
{lesson && lesson.topics && lesson.topics.length > 0 && ( lesson.topics.map((topic, index) => ( )) )}
-

{lesson?.title}

-

{lesson?.summary && ( -

- {lesson.summary.split('\n').map((line, index) => ( -

{line}

- ))} -
- )} -

- {lesson?.additionalLinks && lesson.additionalLinks.length > 0 && ( -
-
-
-

External links:

- -
-
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
-
+
{lesson?.summary && ( +
+ {lesson.summary.split('\n').map((line, index) => ( +

{line}

+ ))}
)} - {!lesson?.additionalLinks || lesson.additionalLinks.length === 0 && ( -
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} +
+
@@ -182,9 +192,6 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete height={194} className="w-[344px] h-[194px] object-cover object-top rounded-lg" /> -
- -
)}
diff --git a/src/components/content/courses/DocumentLesson.js b/src/components/content/courses/DocumentLesson.js index 18c7a26..6919fa0 100644 --- a/src/components/content/courses/DocumentLesson.js +++ b/src/components/content/courses/DocumentLesson.js @@ -4,7 +4,6 @@ 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"; @@ -12,8 +11,8 @@ import dynamic from "next/dynamic"; import useWindowWidth from "@/hooks/useWindowWidth"; import appConfig from "@/config/appConfig"; import useTrackDocumentLesson from "@/hooks/tracking/useTrackDocumentLesson"; -import { Menu } from "primereact/menu"; import { Toast } from "primereact/toast"; +import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -66,6 +65,20 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple }); } } + }, + { + label: 'Open lesson', + icon: 'pi pi-arrow-up-right', + command: () => { + window.open(`/details/${lesson.id}`, '_blank'); + } + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } } ]; @@ -131,13 +144,18 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple

{lesson.title}

-
- {lesson.topics && lesson.topics.length > 0 && ( - lesson.topics.map((topic, index) => ( - - )) - )} -
+ +
+
+ {lesson.topics && lesson.topics.length > 0 && ( + lesson.topics.map((topic, index) => ( + + )) + )}
{lesson.summary && (
@@ -147,7 +165,7 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
)}
-
+
avatar image

- -
-
- { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> +
+ +
- {lesson?.additionalLinks && lesson.additionalLinks.length > 0 && ( -
-
-
-

External links:

- -
-
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
-
-
- )} - {!lesson?.additionalLinks || lesson.additionalLinks.length === 0 && ( -
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
- )} + {renderContent()}
- {renderContent()}
) } diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js index e11c012..9086996 100644 --- a/src/components/content/courses/VideoLesson.js +++ b/src/components/content/courses/VideoLesson.js @@ -3,7 +3,6 @@ import { Tag } from "primereact/tag"; import Image from "next/image"; import ZapDisplay from "@/components/zaps/ZapDisplay"; import { useImageProxy } from "@/hooks/useImageProxy"; -import GenericButton from "@/components/buttons/GenericButton"; import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery"; import { nip19 } from "nostr-tools"; import { getTotalFromZaps } from "@/utils/lightning"; @@ -12,8 +11,8 @@ import { Divider } from "primereact/divider"; import appConfig from "@/config/appConfig"; import useWindowWidth from "@/hooks/useWindowWidth"; import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson'; -import { Menu } from "primereact/menu"; import { Toast } from "primereact/toast"; +import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -68,6 +67,20 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }); } } + }, + { + label: 'Open lesson', + icon: 'pi pi-arrow-up-right', + command: () => { + window.open(`/details/${lesson.id}`, '_blank'); + } + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } } ]; @@ -184,16 +197,22 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
-
+

{lesson.title}

+ +
+
{lesson.topics && lesson.topics.length > 0 && ( lesson.topics.map((topic, index) => ( - + )) )}
-
-
{lesson.summary && ( +
{lesson.summary && (
{lesson.summary.split('\n').map((line, index) => (

{line}

@@ -201,83 +220,31 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
)}
- + -
-
- - { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> -
-
- {lesson?.additionalLinks && lesson.additionalLinks.length > 0 && ( -
-
-
-

External links:

- -
-
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
-
-
- )} - {!lesson?.additionalLinks || lesson.additionalLinks.length === 0 && ( -
- - menuRef.current.toggle(e)} - aria-label="More options" - className="p-button-text" - tooltip={isMobileView ? null : "More options"} - tooltipOptions={{ position: 'top' }} - /> -
- )}
) diff --git a/src/components/content/documents/DocumentDetails.js b/src/components/content/documents/DocumentDetails.js index 6bc2617..491bb30 100644 --- a/src/components/content/documents/DocumentDetails.js +++ b/src/components/content/documents/DocumentDetails.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import axios from "axios"; import { useToast } from "@/hooks/useToast"; import { Tag } from "primereact/tag"; @@ -13,6 +13,8 @@ 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"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -31,25 +33,8 @@ const DocumentDetails = ({ processedEvent, topics, title, summary, image, price, const { showToast } = useToast(); const windowWidth = useWindowWidth(); const isMobileView = windowWidth <= 768; - - useEffect(() => { - if (zaps.length > 0) { - const total = getTotalFromZaps(zaps, processedEvent); - setZapAmount(total); - } - }, [zaps, processedEvent]); - - 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]); + const menuRef = useRef(null); + const toastRef = useRef(null); const handleDelete = async () => { try { @@ -70,6 +55,63 @@ const DocumentDetails = ({ processedEvent, topics, title, summary, image, price, } } + const authorMenuItems = [ + { + label: 'Edit', + icon: 'pi pi-pencil', + command: () => router.push(`/details/${processedEvent.id}/edit`) + }, + { + label: 'Delete', + icon: 'pi pi-trash', + command: handleDelete + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } + } + ]; + + const userMenuItems = [ + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } + } + ]; + + if (course) { + userMenuItems.unshift({ + label: isMobileView ? 'Course' : 'Open Course', + icon: 'pi pi-external-link', + command: () => window.open(`/course/${course}`, '_blank') + }); + } + + useEffect(() => { + if (zaps.length > 0) { + const total = getTotalFromZaps(zaps, processedEvent); + setZapAmount(total); + } + }, [zaps, processedEvent]); + + 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]); + const renderPaymentMessage = () => { if (session?.user && session.user?.role?.subscribed && decryptedContent) { return @@ -127,6 +169,7 @@ const DocumentDetails = ({ processedEvent, topics, title, summary, image, price, return (
+
background image

{title}

-
- {topics && topics.length > 0 && ( - topics.map((topic, index) => ( - - )) - )} - {isLesson && } -
+ +
+
+ {topics && topics.length > 0 && ( + topics.map((topic, index) => ( + + )) + )} + {isLesson && }
{(summary)?.split('\n').map((line, index) => (

{line}

))} - {processedEvent?.additionalLinks && processedEvent?.additionalLinks.length > 0 && ( -
-

Additional Links:

- {processedEvent.additionalLinks.map((link, index) => ( - - ))} -
- )} -
+
avatar image

- +
+ +
-
- {authorView ? ( -
- {renderPaymentMessage()} -
- router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined /> - - { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> -
-
- ) : ( -
- {renderPaymentMessage()} -
- {course && window.open(`/course/${course}`, '_blank')} label={isMobileView ? "Course" : "Open Course"} tooltip="This is a lesson in a course" tooltipOptions={{ position: 'top' }} />} - { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> -
-
- )} +
+ {renderPaymentMessage()}
{renderContent()} diff --git a/src/components/content/videos/VideoDetails.js b/src/components/content/videos/VideoDetails.js index b2bd5bd..167e698 100644 --- a/src/components/content/videos/VideoDetails.js +++ b/src/components/content/videos/VideoDetails.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import axios from "axios"; import { useToast } from "@/hooks/useToast"; import { Tag } from "primereact/tag"; @@ -13,6 +13,8 @@ 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"; const MDDisplay = dynamic( () => import("@uiw/react-markdown-preview"), @@ -31,25 +33,8 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au 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 menuRef = useRef(null); + const toastRef = useRef(null); const handleDelete = async () => { try { @@ -70,6 +55,63 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au } } + const authorMenuItems = [ + { + label: 'Edit', + icon: 'pi pi-pencil', + command: () => router.push(`/details/${processedEvent.id}/edit`) + }, + { + label: 'Delete', + icon: 'pi pi-trash', + command: handleDelete + }, + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } + } + ]; + + const userMenuItems = [ + { + label: 'View Nostr note', + icon: 'pi pi-globe', + command: () => { + window.open(`https://habla.news/a/${nAddress}`, '_blank'); + } + } + ]; + + if (course) { + userMenuItems.unshift({ + label: isMobileView ? 'Course' : 'Open Course', + icon: 'pi pi-external-link', + command: () => window.open(`/course/${course}`, '_blank') + }); + } + + 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 renderPaymentMessage = () => { if (session?.user && session.user?.role?.subscribed && decryptedContent) { return @@ -130,37 +172,9 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au return null; } - const renderAdditionalLinks = () => { - if (processedEvent?.additionalLinks && processedEvent.additionalLinks.length > 0) { - return ( -
-

Additional Links:

- {processedEvent.additionalLinks.map((link, index) => ( - - ))} -
- ); - } - return null; - }; - return (
+ {renderContent()}
@@ -183,65 +197,39 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
-
+
{(summary)?.split('\n').map((line, index) => (

{line}

))} - {renderAdditionalLinks()}
-
- avatar image -

- By{' '} - - {author?.username} - -

+
+
+ avatar image +

+ By{' '} + + {author?.username} + +

+
+
+ +
-
- {authorView ? ( -
- {renderPaymentMessage()} -
- router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined /> - - { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> -
-
- ) : ( -
- {renderPaymentMessage()} -
- {course && window.open(`/course/${course}`, '_blank')} label={isMobileView ? "Course" : "Open Course"} tooltip="This is a lesson in a course" tooltipOptions={{ position: 'top' }} />} - { - window.open(`https://habla.news/a/${nAddress}`, '_blank'); - }} - /> -
-
- )} +
+ {renderPaymentMessage()}
diff --git a/src/components/ui/MoreOptionsMenu.js b/src/components/ui/MoreOptionsMenu.js new file mode 100644 index 0000000..de13f99 --- /dev/null +++ b/src/components/ui/MoreOptionsMenu.js @@ -0,0 +1,69 @@ +import React, { useRef } from "react"; +import { Menu } from "primereact/menu"; +import GenericButton from "@/components/buttons/GenericButton"; + +/** + * A reusable component for displaying a "more options" menu with optional additional links section + * + * @param {Object} props - Component props + * @param {Array} props.menuItems - Array of primary menu items + * @param {Array} props.additionalLinks - Array of additional links to add to the menu + * @param {boolean} props.isMobileView - Whether the view is mobile + * @param {function} props.onLinkClick - Function to be called when a link is clicked + */ +const MoreOptionsMenu = ({ + menuItems, + additionalLinks = [], + isMobileView = false, + onLinkClick = (url) => window.open(url, '_blank') +}) => { + const menuRef = useRef(null); + + // Create a copy of the menu items + const updatedMenuItems = [...menuItems]; + + // Add a separator and additional links if they exist + if (additionalLinks && additionalLinks.length > 0) { + // Add separator + updatedMenuItems.push({ separator: true, className: "my-2" }); + + // Add header for additional links + updatedMenuItems.push({ + label: 'EXTERNAL LINKS', + disabled: true, + className: 'text-sm font-semibold text-gray-400' + }); + + // Add each additional link + additionalLinks.forEach((link, index) => { + let hostname; + try { + hostname = new URL(link).hostname; + } catch (e) { + hostname = link; // Fallback if URL parsing fails + } + + updatedMenuItems.push({ + label: `${hostname}`, + icon: 'pi pi-external-link', + command: () => onLinkClick(link) + }); + }); + } + + return ( +
+ + menuRef.current.toggle(e)} + aria-label="More options" + className="p-button-text" + tooltip={isMobileView ? null : "More options"} + tooltipOptions={{ position: 'top' }} + /> +
+ ); +}; + +export default MoreOptionsMenu; \ No newline at end of file