diff --git a/src/components/content/courses/lessons/CourseLesson.js b/src/components/content/courses/lessons/CourseLesson.js deleted file mode 100644 index 07230f3..0000000 --- a/src/components/content/courses/lessons/CourseLesson.js +++ /dev/null @@ -1,206 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { Tag } from 'primereact/tag'; -import Image from 'next/image'; -import { useImageProxy } from '@/hooks/useImageProxy'; -import { getTotalFromZaps } from '@/utils/lightning'; -import ZapDisplay from '@/components/zaps/ZapDisplay'; -import { useZapsQuery } from '@/hooks/nostrQueries/zaps/useZapsQuery'; -import { Toast } from 'primereact/toast'; -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'; -import { useSession } from 'next-auth/react'; -import MarkdownDisplay from '@/components/markdown/MarkdownDisplay'; - -const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => { - const [zapAmount, setZapAmount] = useState(0); - const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: 'lesson' }); - const { returnImageProxy } = useImageProxy(); - const menuRef = useRef(null); - const toastRef = useRef(null); - const windowWidth = useWindowWidth(); - const isMobileView = windowWidth <= 768; - const { data: session } = useSession(); - - const readTime = lesson?.content ? Math.max(30, Math.ceil(lesson.content.length / 20)) : 60; - - const { isCompleted, isTracking, markLessonAsCompleted } = useTrackDocumentLesson({ - lessonId: lesson?.d, - courseId: course?.d, - readTime, - paidCourse: isPaid, - decryptionPerformed, - }); - - const buildMenuItems = () => { - const items = []; - - const hasAccess = - session?.user && (!isPaid || decryptionPerformed || session.user.role?.subscribed); - - if (hasAccess) { - items.push({ - label: 'Mark as completed', - icon: 'pi pi-check-circle', - command: async () => { - try { - await markLessonAsCompleted(); - setCompleted && setCompleted(lesson.id); - toastRef.current.show({ - severity: 'success', - summary: 'Success', - detail: 'Lesson marked as completed', - life: 3000, - }); - } catch (error) { - console.error('Failed to mark lesson as completed:', error); - toastRef.current.show({ - severity: 'error', - summary: 'Error', - detail: 'Failed to mark lesson as completed', - life: 3000, - }); - } - }, - }); - } - - items.push({ - label: 'Open lesson', - icon: 'pi pi-arrow-up-right', - command: () => { - window.open(`/details/${lesson.id}`, '_blank'); - }, - }); - - items.push({ - 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'); - } - }, - }); - - return items; - }; - - useEffect(() => { - if (!zaps || zapsLoading || zapsError) return; - - const total = getTotalFromZaps(zaps, lesson); - - setZapAmount(total); - }, [zaps, zapsLoading, zapsError, lesson]); - - useEffect(() => { - if (isCompleted && !isTracking && setCompleted) { - setCompleted(lesson.id); - } - }, [isCompleted, isTracking, lesson.id, setCompleted]); - - const renderContent = () => { - if (isPaid && decryptionPerformed) { - return ; - } - if (isPaid && !decryptionPerformed) { - return ( -

- This content is paid and needs to be purchased before viewing. -

- ); - } - if (lesson?.content) { - return ; - } - return null; - }; - - return ( -
- -
-
-
-
-

{lesson?.title}

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

{line}

- ))} -
- )} -
-
- -
- -
-
-
-
- {lesson && ( -
- course thumbnail -
- )} -
-
-
-
- {renderContent()} -
-
- ); -}; - -export default CourseLesson; diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx deleted file mode 100644 index 5a78dec..0000000 --- a/src/components/ui/badge.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { cva } from 'class-variance-authority'; - -import { cn } from '@/utils/tw'; - -const badgeVariants = cva( - 'inline-flex items-center rounded-full border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-300', - { - variants: { - variant: { - default: - 'border-transparent bg-neutral-900 text-neutral-50 hover:bg-neutral-900/80 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/80', - secondary: - 'border-transparent bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80', - destructive: - 'border-transparent bg-red-500 text-neutral-50 hover:bg-red-500/80 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/80', - outline: 'text-neutral-950 dark:text-neutral-50', - }, - }, - defaultVariants: { - variant: 'default', - }, - } -); - -function Badge({ className, variant, ...props }) { - return
; -} - -export { Badge, badgeVariants }; diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx deleted file mode 100644 index 009b2b8..0000000 --- a/src/components/ui/button.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva } from 'class-variance-authority'; - -import { cn } from '@/utils/tw'; - -const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300', - { - variants: { - variant: { - default: - 'bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90', - destructive: - 'bg-red-500 text-neutral-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90', - outline: - 'border border-neutral-200 bg-white hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50', - secondary: - 'bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80', - ghost: - 'hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50', - link: 'text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50', - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - } -); - -const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - return ; -}); -Button.displayName = 'Button'; - -export { Button, buttonVariants }; diff --git a/src/hooks/courses/index.js b/src/hooks/courses/index.js deleted file mode 100644 index 82bc2e2..0000000 --- a/src/hooks/courses/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import useCourseDecryption from '../encryption/useCourseDecryption'; -import useCourseTabs from './useCourseTabs'; -import useCoursePayment from './useCoursePayment'; -import useCourseData from './useCourseData'; -import useLessons from './useLessons'; -import useCourseNavigation from './useCourseNavigation'; -import useCourseTabsState from './useCourseTabsState'; - -export { - useCourseDecryption, - useCourseTabs, - useCoursePayment, - useCourseData, - useLessons, - useCourseNavigation, - useCourseTabsState -}; \ No newline at end of file diff --git a/src/hooks/courses/useCoursePayment.js b/src/hooks/courses/useCoursePayment.js deleted file mode 100644 index c2740b8..0000000 --- a/src/hooks/courses/useCoursePayment.js +++ /dev/null @@ -1,79 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useToast } from '../useToast'; -import { useSession } from 'next-auth/react'; - -/** - * Hook to handle course payment processing and authorization - * @param {Object} course - The course object - * @returns {Object} Payment handling utilities and authorization state - */ -const useCoursePayment = (course) => { - const { data: session, update } = useSession(); - const { showToast } = useToast(); - - // Determine if course requires payment - const isPaidCourse = useMemo(() => { - return course?.price && course.price > 0; - }, [course]); - - // Check if user is authorized to access the course - const isAuthorized = useMemo(() => { - if (!session?.user || !course) return !isPaidCourse; // Free courses are always authorized - - return ( - // User is subscribed - session.user.role?.subscribed || - // User is the creator of the course - session.user.pubkey === course.pubkey || - // Course is free - !isPaidCourse || - // User has purchased this specific course - session.user.purchased?.some(purchase => purchase.courseId === course.d) - ); - }, [session, course, isPaidCourse]); - - // Handler for successful payment - const handlePaymentSuccess = useCallback(async (response) => { - if (response?.preimage) { - try { - await update(); // refresh session - showToast( - 'success', - 'Payment Success', - 'You have successfully purchased this course' - ); - return true; - } catch (err) { - showToast( - 'warn', - 'Session Refresh Failed', - 'Purchase succeeded but we could not refresh your session automatically. Please reload the page.' - ); - return false; - } - } else { - showToast('error', 'Error', 'Failed to purchase course. Please try again.'); - return false; - } - }, [update, showToast]); - - // Handler for payment errors - const handlePaymentError = useCallback((error) => { - showToast( - 'error', - 'Payment Error', - `Failed to purchase course. Please try again. Error: ${error}` - ); - return false; - }, [showToast]); - - return { - isPaidCourse, - isAuthorized, - handlePaymentSuccess, - handlePaymentError, - session - }; -}; - -export default useCoursePayment; \ No newline at end of file diff --git a/src/hooks/courses/useCourseTabs.js b/src/hooks/courses/useCourseTabs.js deleted file mode 100644 index f0ab071..0000000 --- a/src/hooks/courses/useCourseTabs.js +++ /dev/null @@ -1,92 +0,0 @@ -import { useEffect, useCallback } from 'react'; -import { useRouter } from 'next/router'; -import useWindowWidth from '../useWindowWidth'; -import useCourseTabsState from './useCourseTabsState'; - -/** - * @deprecated Use useCourseTabsState for pure state or useCourseNavigation for router integration - * Hook to manage course tabs, navigation, and sidebar visibility - * @param {Object} options - Configuration options - * @param {Array} options.tabMap - Optional custom tab map to use - * @param {boolean} options.initialSidebarVisible - Initial sidebar visibility state - * @returns {Object} Tab management utilities and state - */ -const useCourseTabs = (options = {}) => { - const router = useRouter(); - const windowWidth = useWindowWidth(); - const isMobileView = typeof windowWidth === 'number' ? windowWidth <= 968 : false; - - // Use the base hook for core tab state functionality - const { - activeTab, - setActiveTab, - sidebarVisible, - setSidebarVisible, - tabMap, - getActiveTabIndex, - getTabItems, - toggleSidebar - } = useCourseTabsState({ - tabMap: options.tabMap, - initialSidebarVisible: options.initialSidebarVisible, - isMobileView - }); - - // Update tabs and sidebar based on router query - useEffect(() => { - if (router.isReady) { - const { active, tab } = router.query; - - // If tab is specified in the URL, use that - if (tab && tabMap.includes(tab)) { - setActiveTab(tab); - } else if (active !== undefined) { - // If we have an active lesson, switch to content tab - setActiveTab('content'); - } else { - // Default to overview tab when no parameters - setActiveTab('overview'); - } - } - }, [router.isReady, router.query, tabMap, setActiveTab]); - - // Toggle between tabs with router integration - const toggleTab = useCallback((indexOrName) => { - const tabName = typeof indexOrName === 'number' - ? tabMap[indexOrName] - : indexOrName; - - setActiveTab(tabName); - - // Only show/hide sidebar on mobile - desktop keeps sidebar visible - if (isMobileView) { - setSidebarVisible(tabName === 'lessons'); - } - - // Sync URL with tab change using shallow routing - const newQuery = { - ...router.query, - tab: tabName === 'overview' ? undefined : tabName - }; - router.push( - { pathname: router.pathname, query: newQuery }, - undefined, - { shallow: true } - ); - }, [tabMap, isMobileView, router, setActiveTab, setSidebarVisible]); - - return { - activeTab, - setActiveTab, - sidebarVisible, - setSidebarVisible, - isMobileView, - toggleTab, - toggleSidebar, - getActiveTabIndex, - getTabItems, - tabMap - }; -}; - -export default useCourseTabs; \ No newline at end of file diff --git a/src/hooks/nostrQueries/content/useAllContentQuery.js b/src/hooks/nostrQueries/content/useAllContentQuery.js deleted file mode 100644 index 522a4ca..0000000 --- a/src/hooks/nostrQueries/content/useAllContentQuery.js +++ /dev/null @@ -1,45 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useNDKContext } from '@/context/NDKContext'; - -export function useAllContentQuery({ ids }) { - const [isClient, setIsClient] = useState(false); - const { ndk, addSigner } = useNDKContext(); - - useEffect(() => { - setIsClient(true); - }, []); - - const fetchAllContentFromNDK = async ids => { - try { - await ndk.connect(); - - const filter = { ids: ids }; - const events = await ndk.fetchEvents(filter); - - if (events && events.size > 0) { - const eventsArray = Array.from(events); - return eventsArray; - } - return []; - } catch (error) { - console.error('Error fetching videos from NDK:', error); - return []; - } - }; - - const { - data: allContent, - isLoading: allContentLoading, - error: allContentError, - refetch: refetchAllContent, - } = useQuery({ - queryKey: ['allContent', isClient], - queryFn: () => fetchAllContentFromNDK(ids), - staleTime: 1000 * 60 * 30, // 30 minutes - refetchInterval: 1000 * 60 * 30, // 30 minutes - enabled: isClient, - }); - - return { allContent, allContentLoading, allContentError, refetchAllContent }; -} diff --git a/src/hooks/nostrQueries/content/useCoursesQuery.js b/src/hooks/nostrQueries/content/useCoursesQuery.js deleted file mode 100644 index 28566c0..0000000 --- a/src/hooks/nostrQueries/content/useCoursesQuery.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useNDKContext } from '@/context/NDKContext'; -import axios from 'axios'; -import appConfig from '@/config/appConfig'; - -export function useCoursesQuery() { - const [isClient, setIsClient] = useState(false); - const { ndk, addSigner } = useNDKContext(); - - useEffect(() => { - setIsClient(true); - }, []); - - const hasRequiredProperties = (event, contentIds) => { - const hasId = event.tags.some(([tag, value]) => tag === 'd' && contentIds.includes(value)); - return hasId; - }; - - const fetchCoursesFromNDK = async () => { - try { - const response = await axios.get(`/api/content/all`); - const contentIds = response.data; - - if (!contentIds || contentIds.length === 0) { - return []; // Return early if no content IDs are found - } - - await ndk.connect(); - - const filter = { kinds: [30004], authors: appConfig.authorPubkeys }; - const events = await ndk.fetchEvents(filter); - - if (events && events.size > 0) { - const eventsArray = Array.from(events); - const courses = eventsArray.filter(event => hasRequiredProperties(event, contentIds)); - return courses; - } - return []; - } catch (error) { - console.error('Error fetching courses from NDK:', error); - return []; - } - }; - - const { - data: courses, - isLoading: coursesLoading, - error: coursesError, - refetch: refetchCourses, - } = useQuery({ - queryKey: ['courses', isClient], - queryFn: fetchCoursesFromNDK, - // staleTime: 1000 * 60 * 30, // 30 minutes - // refetchInterval: 1000 * 60 * 30, // 30 minutes - enabled: isClient, - }); - - return { courses, coursesLoading, coursesError, refetchCourses }; -} diff --git a/src/hooks/nostrQueries/content/useDocumentsQuery.js b/src/hooks/nostrQueries/content/useDocumentsQuery.js deleted file mode 100644 index 4183155..0000000 --- a/src/hooks/nostrQueries/content/useDocumentsQuery.js +++ /dev/null @@ -1,61 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useNDKContext } from '@/context/NDKContext'; -import axios from 'axios'; -import appConfig from '@/config/appConfig'; - -export function useDocumentsQuery() { - const [isClient, setIsClient] = useState(false); - const { ndk, addSigner } = useNDKContext(); - - useEffect(() => { - setIsClient(true); - }, []); - - const hasRequiredProperties = (event, contentIds) => { - const hasDocument = event.tags.some(([tag, value]) => tag === 't' && value === 'document'); - const hasId = event.tags.some(([tag, value]) => tag === 'd' && contentIds.includes(value)); - return hasDocument && hasId; - }; - - const fetchDocumentsFromNDK = async () => { - try { - const response = await axios.get(`/api/content/all`); - const contentIds = response.data; - - if (!contentIds || contentIds.length === 0) { - return []; // Return early if no content IDs are found - } - - await ndk.connect(); - - const filter = { kinds: [30023, 30402], authors: appConfig.authorPubkeys }; - const events = await ndk.fetchEvents(filter); - - if (events && events.size > 0) { - const eventsArray = Array.from(events); - const documents = eventsArray.filter(event => hasRequiredProperties(event, contentIds)); - return documents; - } - return []; - } catch (error) { - console.error('Error fetching documents from NDK:', error); - return []; - } - }; - - const { - data: documents, - isLoading: documentsLoading, - error: documentsError, - refetch: refetchDocuments, - } = useQuery({ - queryKey: ['documents', isClient], - queryFn: fetchDocumentsFromNDK, - // staleTime: 1000 * 60 * 30, // 30 minutes - // refetchInterval: 1000 * 60 * 30, // 30 minutes - enabled: isClient, - }); - - return { documents, documentsLoading, documentsError, refetchDocuments }; -} diff --git a/src/hooks/nostrQueries/content/useVideosQuery.js b/src/hooks/nostrQueries/content/useVideosQuery.js deleted file mode 100644 index 8797f1e..0000000 --- a/src/hooks/nostrQueries/content/useVideosQuery.js +++ /dev/null @@ -1,61 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useNDKContext } from '@/context/NDKContext'; -import axios from 'axios'; -import appConfig from '@/config/appConfig'; - -export function useVideosQuery() { - const [isClient, setIsClient] = useState(false); - const { ndk, addSigner } = useNDKContext(); - - useEffect(() => { - setIsClient(true); - }, []); - - const hasRequiredProperties = (event, contentIds) => { - const hasVideo = event.tags.some(([tag, value]) => tag === 't' && value === 'video'); - const hasId = event.tags.some(([tag, value]) => tag === 'd' && contentIds.includes(value)); - return hasVideo && hasId; - }; - - const fetchVideosFromNDK = async () => { - try { - const response = await axios.get(`/api/content/all`); - const contentIds = response.data; - - if (!contentIds || contentIds.length === 0) { - return []; // Return early if no content IDs are found - } - - await ndk.connect(); - - const filter = { kinds: [30023, 30402], authors: appConfig.authorPubkeys }; - const events = await ndk.fetchEvents(filter); - - if (events && events.size > 0) { - const eventsArray = Array.from(events); - const videos = eventsArray.filter(event => hasRequiredProperties(event, contentIds)); - return videos; - } - return []; - } catch (error) { - console.error('Error fetching videos from NDK:', error); - return []; - } - }; - - const { - data: videos, - isLoading: videosLoading, - error: videosError, - refetch: refetchVideos, - } = useQuery({ - queryKey: ['videos', isClient], - queryFn: fetchVideosFromNDK, - // staleTime: 1000 * 60 * 30, // 30 minutes - // refetchInterval: 1000 * 60 * 30, // 30 minutes - enabled: isClient, - }); - - return { videos, videosLoading, videosError, refetchVideos }; -}