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 && (
-
-
-
- )}
-
-
-
-
- {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 };
-}