mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
342 lines
11 KiB
JavaScript
342 lines
11 KiB
JavaScript
import React, { useEffect, useState, useCallback } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { findKind0Fields } from '@/utils/nostr';
|
|
import { useNDKContext } from '@/context/NDKContext';
|
|
import { useSession } from 'next-auth/react';
|
|
import { nip19 } from 'nostr-tools';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
|
|
|
// Hooks
|
|
import useCourseDecryption from '@/hooks/encryption/useCourseDecryption';
|
|
import useCourseData from '@/hooks/courses/useCourseData';
|
|
import useLessons from '@/hooks/courses/useLessons';
|
|
import useCourseNavigation from '@/hooks/courses/useCourseNavigation';
|
|
import useWindowWidth from '@/hooks/useWindowWidth';
|
|
|
|
// Components
|
|
import CourseSidebar from '@/components/content/courses/layout/CourseSidebar';
|
|
import CourseContent from '@/components/content/courses/tabs/CourseContent';
|
|
import CourseQA from '@/components/content/courses/tabs/CourseQA';
|
|
import CourseOverview from '@/components/content/courses/tabs/CourseOverview';
|
|
import MenuTab from '@/components/menutab/MenuTab';
|
|
|
|
// Config
|
|
import appConfig from '@/config/appConfig';
|
|
|
|
const Course = () => {
|
|
const router = useRouter();
|
|
const { ndk, addSigner } = useNDKContext();
|
|
const { data: session, update } = useSession();
|
|
const { showToast } = useToast();
|
|
const [completedLessons, setCompletedLessons] = useState([]);
|
|
const [nAddresses, setNAddresses] = useState({});
|
|
const [nsec, setNsec] = useState(null);
|
|
const [npub, setNpub] = useState(null);
|
|
const [nAddress, setNAddress] = useState(null);
|
|
const [isDecrypting, setIsDecrypting] = useState(false);
|
|
const windowWidth = useWindowWidth();
|
|
const isMobileView = windowWidth <= 968;
|
|
const navbarHeight = 60; // Match the height from Navbar component
|
|
|
|
// Use our navigation hook
|
|
const {
|
|
activeIndex,
|
|
activeTab,
|
|
sidebarVisible,
|
|
setSidebarVisible,
|
|
handleLessonSelect,
|
|
toggleTab,
|
|
toggleSidebar,
|
|
getActiveTabIndex,
|
|
getTabItems,
|
|
} = useCourseNavigation(router, isMobileView);
|
|
|
|
useEffect(() => {
|
|
if (router.isReady && router.query.slug) {
|
|
const { slug } = router.query;
|
|
if (slug.includes('naddr')) {
|
|
setNAddress(slug);
|
|
} else {
|
|
console.warn('No naddress found in slug');
|
|
showToast('error', 'Error', 'Course identifier not found in URL');
|
|
setTimeout(() => {
|
|
router.push('/courses'); // Redirect to courses page
|
|
}, 3000);
|
|
}
|
|
}
|
|
}, [router.isReady, router.query.slug, showToast, router]);
|
|
|
|
// Load completed lessons from localStorage when course is loaded
|
|
useEffect(() => {
|
|
if (router.isReady && router.query.slug && session?.user) {
|
|
const courseId = router.query.slug;
|
|
const storageKey = `course_${courseId}_${session.user.pubkey}_completed`;
|
|
const savedCompletedLessons = localStorage.getItem(storageKey);
|
|
|
|
if (savedCompletedLessons) {
|
|
try {
|
|
const parsedLessons = JSON.parse(savedCompletedLessons);
|
|
setCompletedLessons(parsedLessons);
|
|
} catch (error) {
|
|
console.error('Error parsing completed lessons from storage:', error);
|
|
}
|
|
}
|
|
}
|
|
}, [router.isReady, router.query.slug, session]);
|
|
|
|
const setCompleted = useCallback(lessonId => {
|
|
setCompletedLessons(prev => {
|
|
// Avoid duplicates
|
|
if (prev.includes(lessonId)) {
|
|
return prev;
|
|
}
|
|
|
|
const newCompletedLessons = [...prev, lessonId];
|
|
|
|
// Save to localStorage
|
|
if (router.query.slug && session?.user) {
|
|
const courseId = router.query.slug;
|
|
const storageKey = `course_${courseId}_${session.user.pubkey}_completed`;
|
|
localStorage.setItem(storageKey, JSON.stringify(newCompletedLessons));
|
|
}
|
|
|
|
return newCompletedLessons;
|
|
});
|
|
}, [router.query.slug, session]);
|
|
|
|
const fetchAuthor = useCallback(
|
|
async pubkey => {
|
|
const author = await ndk.getUser({ pubkey });
|
|
const profile = await author.fetchProfile();
|
|
const fields = await findKind0Fields(profile);
|
|
return fields;
|
|
},
|
|
[ndk]
|
|
);
|
|
|
|
const {
|
|
course,
|
|
lessonIds,
|
|
paidCourse,
|
|
loading: courseLoading,
|
|
} = useCourseData(ndk, fetchAuthor, router);
|
|
|
|
const { lessons, uniqueLessons, setLessons } = useLessons(
|
|
ndk,
|
|
fetchAuthor,
|
|
lessonIds,
|
|
course?.pubkey
|
|
);
|
|
|
|
const { decryptionPerformed, loading: decryptionLoading, decryptedLessonIds } = useCourseDecryption(
|
|
session,
|
|
paidCourse,
|
|
course,
|
|
lessons,
|
|
setLessons,
|
|
router,
|
|
activeIndex
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (paidCourse && uniqueLessons.length > 0) {
|
|
const currentLesson = uniqueLessons[activeIndex];
|
|
if (currentLesson && !decryptedLessonIds[currentLesson.id]) {
|
|
setIsDecrypting(true);
|
|
} else {
|
|
setIsDecrypting(false);
|
|
}
|
|
} else {
|
|
setIsDecrypting(false);
|
|
}
|
|
}, [activeIndex, uniqueLessons, decryptedLessonIds, paidCourse]);
|
|
|
|
useEffect(() => {
|
|
if (uniqueLessons.length > 0) {
|
|
const addresses = {};
|
|
uniqueLessons.forEach(lesson => {
|
|
const addr = nip19.naddrEncode({
|
|
pubkey: lesson.pubkey,
|
|
kind: lesson.kind,
|
|
identifier: lesson.d,
|
|
relays: appConfig.defaultRelayUrls,
|
|
});
|
|
addresses[lesson.id] = addr;
|
|
});
|
|
setNAddresses(addresses);
|
|
}
|
|
}, [uniqueLessons]);
|
|
|
|
useEffect(() => {
|
|
if (session?.user?.privkey) {
|
|
const privkeyBuffer = Buffer.from(session.user.privkey, 'hex');
|
|
setNsec(nip19.nsecEncode(privkeyBuffer));
|
|
setNpub(null);
|
|
} else if (session?.user?.pubkey) {
|
|
setNsec(null);
|
|
setNpub(nip19.npubEncode(session.user.pubkey));
|
|
} else {
|
|
setNsec(null);
|
|
setNpub(null);
|
|
}
|
|
}, [session]);
|
|
|
|
const isAuthorized =
|
|
session?.user?.role?.subscribed ||
|
|
session?.user?.pubkey === course?.pubkey ||
|
|
!paidCourse ||
|
|
session?.user?.purchased?.some(purchase => purchase.courseId === course?.d);
|
|
|
|
const handlePaymentSuccess = async response => {
|
|
if (response && response?.preimage) {
|
|
const updated = await update();
|
|
showToast('success', 'Payment Success', 'You have successfully purchased this course');
|
|
} else {
|
|
showToast('error', 'Error', 'Failed to purchase course. Please try again.');
|
|
}
|
|
};
|
|
|
|
const handlePaymentError = error => {
|
|
showToast(
|
|
'error',
|
|
'Payment Error',
|
|
`Failed to purchase course. Please try again. Error: ${error}`
|
|
);
|
|
};
|
|
|
|
if (courseLoading) {
|
|
return (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<ProgressSpinner />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="mx-auto px-8 max-mob:px-0 mb-12 mt-2">
|
|
{/* Tab navigation using MenuTab component */}
|
|
<div className="z-10 bg-transparent"
|
|
style={{
|
|
top: `${navbarHeight}px`,
|
|
height: `${navbarHeight}px`
|
|
}}>
|
|
<MenuTab
|
|
items={getTabItems()}
|
|
activeIndex={getActiveTabIndex()}
|
|
onTabChange={(index) => toggleTab(index)}
|
|
sidebarVisible={sidebarVisible}
|
|
onToggleSidebar={toggleSidebar}
|
|
isMobileView={isMobileView}
|
|
/>
|
|
</div>
|
|
|
|
{/* Main content area with fixed width */}
|
|
<div className="relative mt-4">
|
|
<div className={`transition-all duration-500 ease-in-out ${isMobileView ? 'w-full' : 'w-full'}`}
|
|
style={!isMobileView && sidebarVisible ? {paddingRight: '320px'} : {}}>
|
|
|
|
{/* Overview tab content */}
|
|
<div className={`${activeTab === 'overview' ? 'block' : 'hidden'}`}>
|
|
<CourseOverview
|
|
course={course}
|
|
paidCourse={paidCourse}
|
|
lessons={uniqueLessons}
|
|
decryptionPerformed={decryptionPerformed}
|
|
handlePaymentSuccess={handlePaymentSuccess}
|
|
handlePaymentError={handlePaymentError}
|
|
isMobileView={isMobileView}
|
|
completedLessons={completedLessons}
|
|
/>
|
|
</div>
|
|
|
|
{/* Content tab content */}
|
|
<div className={`${activeTab === 'content' ? 'block' : 'hidden'}`}>
|
|
{isDecrypting || decryptionLoading ? (
|
|
<div className="w-full py-12 bg-gray-800 rounded-lg flex items-center justify-center">
|
|
<div className="text-center">
|
|
<ProgressSpinner style={{ width: '50px', height: '50px' }} />
|
|
<p className="mt-4 text-gray-300">Decrypting lesson content...</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<CourseContent
|
|
lessons={uniqueLessons}
|
|
activeIndex={activeIndex}
|
|
course={course}
|
|
paidCourse={paidCourse}
|
|
decryptedLessonIds={decryptedLessonIds}
|
|
setCompleted={setCompleted}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* QA tab content */}
|
|
<div className={`${activeTab === 'qa' ? 'block' : 'hidden'}`}>
|
|
<CourseQA
|
|
nAddress={nAddress}
|
|
isAuthorized={isAuthorized}
|
|
nsec={nsec}
|
|
npub={npub}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Course Sidebar - positioned absolutely on desktop when visible */}
|
|
{!isMobileView ? (
|
|
<div
|
|
className={`transition-all duration-500 ease-in-out ${
|
|
sidebarVisible ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full'
|
|
}`}
|
|
style={{
|
|
position: 'absolute',
|
|
top: '0',
|
|
right: '0',
|
|
width: '320px',
|
|
height: '100%',
|
|
zIndex: 999,
|
|
overflow: 'visible',
|
|
pointerEvents: sidebarVisible ? 'auto' : 'none'
|
|
}}
|
|
>
|
|
<CourseSidebar
|
|
lessons={uniqueLessons}
|
|
activeIndex={activeIndex}
|
|
onLessonSelect={handleLessonSelect}
|
|
completedLessons={completedLessons}
|
|
isMobileView={isMobileView}
|
|
sidebarVisible={sidebarVisible}
|
|
setSidebarVisible={setSidebarVisible}
|
|
hideToggleButton={true}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className={`flex-shrink-0 transition-all duration-300 z-[999] ${
|
|
(isMobileView && activeTab === 'lessons') ? 'ml-0 w-auto opacity-100' :
|
|
'w-0 ml-0 opacity-0 overflow-hidden'
|
|
}`}>
|
|
<CourseSidebar
|
|
lessons={uniqueLessons}
|
|
activeIndex={activeIndex}
|
|
onLessonSelect={handleLessonSelect}
|
|
completedLessons={completedLessons}
|
|
isMobileView={isMobileView}
|
|
onClose={() => {
|
|
setSidebarVisible(false);
|
|
toggleTab(getActiveTabIndex());
|
|
}}
|
|
sidebarVisible={sidebarVisible}
|
|
setSidebarVisible={setSidebarVisible}
|
|
hideToggleButton={true}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Course;
|