Remove unused course and content hooks

This commit is contained in:
Austin Kelsay 2025-06-11 09:06:56 -05:00
parent 892ac6c7be
commit d31e28f49b
10 changed files with 0 additions and 695 deletions

View File

@ -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 <MarkdownDisplay content={lesson.content} className="p-4 rounded-lg w-full" />;
}
if (isPaid && !decryptionPerformed) {
return (
<p className="text-center text-xl text-red-500">
This content is paid and needs to be purchased before viewing.
</p>
);
}
if (lesson?.content) {
return <MarkdownDisplay content={lesson.content} className="p-4 rounded-lg w-full" />;
}
return null;
};
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">
<Toast ref={toastRef} />
<div className="w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col">
<div className="w-[75vw] mx-auto flex flex-row items-start justify-between max-tab:flex-col max-mob:flex-col max-tab:w-[95vw] max-mob:w-[95vw]">
<div className="flex flex-col items-start max-w-[45vw] max-tab:max-w-[100vw] max-mob:max-w-[100vw]">
<div className="flex flex-row items-center justify-between w-full">
<h1 className="text-4xl">{lesson?.title}</h1>
<ZapDisplay zapAmount={zapAmount} event={lesson} zapsLoading={zapsLoading} />
</div>
<div className="pt-2 flex flex-row justify-start w-full mt-2 mb-4">
{lesson &&
lesson.topics &&
lesson.topics.length > 0 &&
lesson.topics.map((topic, index) => (
<Tag className="mr-2 text-white" key={index} value={topic}></Tag>
))}
</div>
<div className="text-xl mt-6">
{lesson?.summary && (
<div className="text-xl mt-4">
{lesson.summary.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
)}
</div>
<div className="flex items-center justify-between w-full mt-8">
<div className="flex flex-row w-fit items-center">
<Image
alt="avatar thumbnail"
src={returnImageProxy(lesson.author?.avatar, lesson.author?.pubkey)}
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"
>
{lesson.author?.username || lesson.author?.pubkey}
</a>
</p>
</div>
<div className="flex justify-end">
<MoreOptionsMenu
menuItems={buildMenuItems()}
additionalLinks={lesson?.additionalLinks || []}
isMobileView={isMobileView}
/>
</div>
</div>
</div>
<div className="flex flex-col max-tab:mt-12 max-mob:mt-12">
{lesson && (
<div className="flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md">
<Image
alt="course thumbnail"
src={returnImageProxy(lesson.image)}
width={344}
height={194}
className="w-[344px] h-[194px] object-cover object-top rounded-lg"
/>
</div>
)}
</div>
</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:max-w-[100vw] max-mob:max-w-[100vw]">
{renderContent()}
</div>
</div>
);
};
export default CourseLesson;

View File

@ -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 <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@ -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 <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
});
Button.displayName = 'Button';
export { Button, buttonVariants };

View File

@ -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
};

View File

@ -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;

View File

@ -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;

View File

@ -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 };
}

View File

@ -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 };
}

View File

@ -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 };
}

View File

@ -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 };
}