2025-04-13 14:36:55 -05:00
|
|
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
2025-04-17 13:00:58 -05:00
|
|
|
import { useRouter } from 'next/router';
|
|
|
|
import { parseCourseEvent, parseEvent, findKind0Fields } from '@/utils/nostr';
|
|
|
|
import CourseDetails from '@/components/content/courses/CourseDetails';
|
|
|
|
import VideoLesson from '@/components/content/courses/VideoLesson';
|
|
|
|
import DocumentLesson from '@/components/content/courses/DocumentLesson';
|
|
|
|
import CombinedLesson from '@/components/content/courses/CombinedLesson';
|
2025-04-04 10:52:03 -05:00
|
|
|
import CourseSidebar from '@/components/content/courses/CourseSidebar';
|
2025-04-13 14:36:55 -05:00
|
|
|
import CourseHeader from '@/components/content/courses/CourseHeader';
|
2025-04-17 13:00:58 -05:00
|
|
|
import { useNDKContext } from '@/context/NDKContext';
|
|
|
|
import { useSession } from 'next-auth/react';
|
2025-04-02 17:14:39 -05:00
|
|
|
import { nip19 } from 'nostr-tools';
|
2025-04-17 13:00:58 -05:00
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
|
|
|
import { useDecryptContent } from '@/hooks/encryption/useDecryptContent';
|
|
|
|
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
|
|
|
import appConfig from '@/config/appConfig';
|
2025-04-04 10:52:03 -05:00
|
|
|
import useWindowWidth from '@/hooks/useWindowWidth';
|
2025-04-12 13:50:29 -05:00
|
|
|
import MenuTab from '@/components/menutab/MenuTab';
|
2025-04-13 12:43:24 -05:00
|
|
|
import { Tag } from 'primereact/tag';
|
2025-04-13 20:41:21 -05:00
|
|
|
import MarkdownDisplay from '@/components/markdown/MarkdownDisplay';
|
2024-02-27 18:29:57 -06:00
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
const useCourseData = (ndk, fetchAuthor, router) => {
|
2025-04-02 17:47:30 -05:00
|
|
|
const [course, setCourse] = useState(null);
|
|
|
|
const [lessonIds, setLessonIds] = useState([]);
|
|
|
|
const [paidCourse, setPaidCourse] = useState(null);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const { showToast } = useToast();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!router.isReady) return;
|
|
|
|
|
|
|
|
const { slug } = router.query;
|
|
|
|
let id;
|
|
|
|
|
|
|
|
const fetchCourseId = async () => {
|
2025-04-17 13:00:58 -05:00
|
|
|
if (slug.includes('naddr')) {
|
2025-04-02 17:47:30 -05:00
|
|
|
const { data } = nip19.decode(slug);
|
|
|
|
if (!data?.identifier) {
|
2025-04-17 13:00:58 -05:00
|
|
|
showToast('error', 'Error', 'Resource not found');
|
2025-04-02 17:47:30 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return data.identifier;
|
|
|
|
} else {
|
|
|
|
return slug;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-04-17 13:00:58 -05:00
|
|
|
const fetchCourse = async courseId => {
|
2025-04-02 17:47:30 -05:00
|
|
|
try {
|
|
|
|
await ndk.connect();
|
2025-04-17 13:00:58 -05:00
|
|
|
const event = await ndk.fetchEvent({ '#d': [courseId] });
|
2025-04-02 17:47:30 -05:00
|
|
|
if (!event) return null;
|
|
|
|
|
|
|
|
const author = await fetchAuthor(event.pubkey);
|
2025-04-17 13:00:58 -05:00
|
|
|
const lessonIds = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1].split(':')[2]);
|
2025-04-02 17:47:30 -05:00
|
|
|
|
|
|
|
const parsedCourse = { ...parseCourseEvent(event), author };
|
|
|
|
return { parsedCourse, lessonIds };
|
|
|
|
} catch (error) {
|
2025-04-17 13:00:58 -05:00
|
|
|
console.error('Error fetching event:', error);
|
2025-04-02 17:47:30 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const initializeCourse = async () => {
|
|
|
|
setLoading(true);
|
|
|
|
id = await fetchCourseId();
|
|
|
|
if (!id) {
|
|
|
|
setLoading(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const courseData = await fetchCourse(id);
|
|
|
|
if (courseData) {
|
|
|
|
const { parsedCourse, lessonIds } = courseData;
|
|
|
|
setCourse(parsedCourse);
|
|
|
|
setLessonIds(lessonIds);
|
|
|
|
setPaidCourse(parsedCourse.price && parsedCourse.price > 0);
|
|
|
|
}
|
|
|
|
setLoading(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
initializeCourse();
|
|
|
|
}, [router.isReady, router.query, ndk, fetchAuthor, showToast]);
|
|
|
|
|
|
|
|
return { course, lessonIds, paidCourse, loading };
|
2024-09-14 16:43:03 -05:00
|
|
|
};
|
|
|
|
|
2024-09-15 15:15:58 -05:00
|
|
|
const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
|
2025-04-02 17:47:30 -05:00
|
|
|
const [lessons, setLessons] = useState([]);
|
|
|
|
const [uniqueLessons, setUniqueLessons] = useState([]);
|
|
|
|
const { showToast } = useToast();
|
|
|
|
useEffect(() => {
|
|
|
|
if (lessonIds.length > 0) {
|
2025-04-17 13:00:58 -05:00
|
|
|
const fetchLesson = async lessonId => {
|
2025-04-02 17:47:30 -05:00
|
|
|
try {
|
|
|
|
await ndk.connect();
|
2025-04-02 16:38:37 -05:00
|
|
|
const filter = {
|
2025-04-17 13:00:58 -05:00
|
|
|
'#d': [lessonId],
|
2025-04-02 16:38:37 -05:00
|
|
|
kinds: [30023, 30402],
|
|
|
|
authors: [pubkey],
|
|
|
|
};
|
2025-04-02 17:47:30 -05:00
|
|
|
const event = await ndk.fetchEvent(filter);
|
|
|
|
if (event) {
|
|
|
|
const author = await fetchAuthor(event.pubkey);
|
|
|
|
const parsedLesson = { ...parseEvent(event), author };
|
2025-04-17 13:00:58 -05:00
|
|
|
setLessons(prev => {
|
2025-04-02 17:47:30 -05:00
|
|
|
// Check if the lesson already exists in the array
|
2025-04-17 13:00:58 -05:00
|
|
|
const exists = prev.some(lesson => lesson.id === parsedLesson.id);
|
2025-04-02 17:47:30 -05:00
|
|
|
if (!exists) {
|
|
|
|
return [...prev, parsedLesson];
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2025-04-17 13:00:58 -05:00
|
|
|
console.error('Error fetching event:', error);
|
2024-07-30 17:16:09 -05:00
|
|
|
}
|
2025-04-02 17:47:30 -05:00
|
|
|
};
|
2025-04-17 13:00:58 -05:00
|
|
|
lessonIds.forEach(lessonId => fetchLesson(lessonId));
|
2025-04-02 17:47:30 -05:00
|
|
|
}
|
|
|
|
}, [lessonIds, ndk, fetchAuthor, pubkey]);
|
2024-07-30 17:16:09 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
useEffect(() => {
|
|
|
|
const newUniqueLessons = Array.from(
|
2025-04-17 13:00:58 -05:00
|
|
|
new Map(lessons.map(lesson => [lesson.id, lesson])).values()
|
2025-04-02 17:47:30 -05:00
|
|
|
);
|
|
|
|
setUniqueLessons(newUniqueLessons);
|
|
|
|
}, [lessons]);
|
2024-08-25 12:12:55 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
return { lessons, uniqueLessons, setLessons };
|
2024-09-14 16:43:03 -05:00
|
|
|
};
|
2024-08-25 12:12:55 -05:00
|
|
|
|
2024-09-14 16:43:03 -05:00
|
|
|
const useDecryption = (session, paidCourse, course, lessons, setLessons) => {
|
2025-04-02 17:47:30 -05:00
|
|
|
const [decryptionPerformed, setDecryptionPerformed] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const { decryptContent } = useDecryptContent();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const decrypt = async () => {
|
|
|
|
if (session?.user && paidCourse && !decryptionPerformed) {
|
|
|
|
setLoading(true);
|
|
|
|
const canAccess =
|
2025-04-17 13:00:58 -05:00
|
|
|
session.user.purchased?.some(purchase => purchase.courseId === course?.d) ||
|
2025-04-02 17:47:30 -05:00
|
|
|
session.user?.role?.subscribed ||
|
|
|
|
session.user?.pubkey === course?.pubkey;
|
|
|
|
|
|
|
|
if (canAccess && lessons.length > 0) {
|
|
|
|
try {
|
|
|
|
const decryptedLessons = await Promise.all(
|
2025-04-17 13:00:58 -05:00
|
|
|
lessons.map(async lesson => {
|
2025-04-02 17:47:30 -05:00
|
|
|
const decryptedContent = await decryptContent(lesson.content);
|
|
|
|
return { ...lesson, content: decryptedContent };
|
|
|
|
})
|
|
|
|
);
|
|
|
|
setLessons(decryptedLessons);
|
|
|
|
setDecryptionPerformed(true);
|
|
|
|
} catch (error) {
|
2025-04-17 13:00:58 -05:00
|
|
|
console.error('Error decrypting lessons:', error);
|
2025-04-02 17:47:30 -05:00
|
|
|
}
|
2024-08-16 18:00:46 -05:00
|
|
|
}
|
2025-04-02 17:47:30 -05:00
|
|
|
setLoading(false);
|
|
|
|
}
|
|
|
|
setLoading(false);
|
|
|
|
};
|
|
|
|
decrypt();
|
|
|
|
}, [session, paidCourse, course, lessons, decryptionPerformed, setLessons]);
|
2024-09-14 16:43:03 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
return { decryptionPerformed, loading };
|
2024-09-14 16:43:03 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
const Course = () => {
|
2025-04-02 17:47:30 -05:00
|
|
|
const router = useRouter();
|
|
|
|
const { ndk, addSigner } = useNDKContext();
|
|
|
|
const { data: session, update } = useSession();
|
|
|
|
const { showToast } = useToast();
|
2025-04-04 10:52:03 -05:00
|
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
2025-04-02 17:47:30 -05:00
|
|
|
const [completedLessons, setCompletedLessons] = useState([]);
|
2025-04-02 16:38:37 -05:00
|
|
|
const [nAddresses, setNAddresses] = useState({});
|
|
|
|
const [nsec, setNsec] = useState(null);
|
|
|
|
const [npub, setNpub] = useState(null);
|
2025-04-04 10:52:03 -05:00
|
|
|
const [sidebarVisible, setSidebarVisible] = useState(false);
|
2025-04-12 13:50:29 -05:00
|
|
|
const [nAddress, setNAddress] = useState(null);
|
2025-04-04 10:52:03 -05:00
|
|
|
const windowWidth = useWindowWidth();
|
|
|
|
const isMobileView = windowWidth <= 968;
|
2025-04-13 14:36:55 -05:00
|
|
|
const [activeTab, setActiveTab] = useState('overview'); // Default to overview tab
|
|
|
|
const navbarHeight = 60; // Match the height from Navbar component
|
2024-09-14 16:43:03 -05:00
|
|
|
|
2025-04-12 13:50:29 -05:00
|
|
|
useEffect(() => {
|
|
|
|
if (router.isReady) {
|
|
|
|
const { slug } = router.query;
|
|
|
|
if (slug.includes('naddr')) {
|
|
|
|
setNAddress(slug);
|
|
|
|
} else {
|
|
|
|
// todo: no naddress?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [router.isReady, router.query.slug]);
|
|
|
|
|
2025-04-13 14:36:55 -05:00
|
|
|
useEffect(() => {
|
|
|
|
if (router.isReady) {
|
|
|
|
const { active } = router.query;
|
|
|
|
if (active !== undefined) {
|
|
|
|
setActiveIndex(parseInt(active, 10));
|
|
|
|
// If we have an active lesson, switch to content tab
|
|
|
|
setActiveTab('content');
|
|
|
|
} else {
|
|
|
|
setActiveIndex(0);
|
|
|
|
// Default to overview tab when no active parameter
|
|
|
|
setActiveTab('overview');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Auto-open sidebar on desktop, close on mobile
|
|
|
|
setSidebarVisible(!isMobileView);
|
|
|
|
}
|
|
|
|
}, [router.isReady, router.query, isMobileView]);
|
|
|
|
|
2025-04-17 13:00:58 -05:00
|
|
|
const setCompleted = useCallback(lessonId => {
|
|
|
|
setCompletedLessons(prev => [...prev, lessonId]);
|
2025-04-02 17:47:30 -05:00
|
|
|
}, []);
|
2024-09-23 22:44:32 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
const fetchAuthor = useCallback(
|
2025-04-17 13:00:58 -05:00
|
|
|
async pubkey => {
|
2025-04-02 17:47:30 -05:00
|
|
|
const author = await ndk.getUser({ pubkey });
|
|
|
|
const profile = await author.fetchProfile();
|
|
|
|
const fields = await findKind0Fields(profile);
|
|
|
|
return fields;
|
|
|
|
},
|
|
|
|
[ndk]
|
|
|
|
);
|
2024-08-16 18:00:46 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
const {
|
|
|
|
course,
|
|
|
|
lessonIds,
|
|
|
|
paidCourse,
|
|
|
|
loading: courseLoading,
|
|
|
|
} = useCourseData(ndk, fetchAuthor, router);
|
2025-04-13 14:36:55 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
const { lessons, uniqueLessons, setLessons } = useLessons(
|
|
|
|
ndk,
|
|
|
|
fetchAuthor,
|
|
|
|
lessonIds,
|
|
|
|
course?.pubkey
|
|
|
|
);
|
2025-04-13 14:36:55 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
const { decryptionPerformed, loading: decryptionLoading } = useDecryption(
|
|
|
|
session,
|
|
|
|
paidCourse,
|
|
|
|
course,
|
|
|
|
lessons,
|
|
|
|
setLessons
|
|
|
|
);
|
2024-08-16 18:00:46 -05:00
|
|
|
|
2025-04-13 14:36:55 -05:00
|
|
|
// Check if course is completed - moved after course is initialized
|
|
|
|
const isCourseCompleted = useMemo(() => {
|
|
|
|
if (!course || !completedLessons.length) return false;
|
|
|
|
// A course is completed if at least one lesson is completed
|
|
|
|
// You can change this logic if needed (e.g., all lessons must be completed)
|
|
|
|
return completedLessons.length > 0;
|
|
|
|
}, [completedLessons, course]);
|
2024-08-16 18:00:46 -05:00
|
|
|
|
2025-04-02 16:38:37 -05:00
|
|
|
useEffect(() => {
|
|
|
|
if (uniqueLessons.length > 0) {
|
|
|
|
const addresses = {};
|
2025-04-17 13:00:58 -05:00
|
|
|
uniqueLessons.forEach(lesson => {
|
2025-04-02 16:38:37 -05:00
|
|
|
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) {
|
2025-04-17 13:00:58 -05:00
|
|
|
const privkeyBuffer = Buffer.from(session.user.privkey, 'hex');
|
2025-04-02 16:38:37 -05:00
|
|
|
setNsec(nip19.nsecEncode(privkeyBuffer));
|
2025-04-20 16:32:52 -05:00
|
|
|
setNpub(null);
|
2025-04-02 16:38:37 -05:00
|
|
|
} else if (session?.user?.pubkey) {
|
2025-04-20 16:32:52 -05:00
|
|
|
setNsec(null);
|
2025-04-02 16:38:37 -05:00
|
|
|
setNpub(nip19.npubEncode(session.user.pubkey));
|
2025-04-20 16:32:52 -05:00
|
|
|
} else {
|
|
|
|
setNsec(null);
|
|
|
|
setNpub(null);
|
2025-04-02 16:38:37 -05:00
|
|
|
}
|
|
|
|
}, [session]);
|
2025-04-04 10:52:03 -05:00
|
|
|
|
|
|
|
const handleLessonSelect = index => {
|
|
|
|
setActiveIndex(index);
|
|
|
|
router.push(`/course/${router.query.slug}?active=${index}`, undefined, { shallow: true });
|
|
|
|
|
|
|
|
// On mobile, switch to content tab after selection
|
|
|
|
if (isMobileView) {
|
|
|
|
setActiveTab('content');
|
|
|
|
setSidebarVisible(false);
|
2024-08-17 12:56:27 -05:00
|
|
|
}
|
2025-04-02 17:47:30 -05:00
|
|
|
};
|
2024-08-17 12:56:27 -05:00
|
|
|
|
2025-04-17 13:00:58 -05:00
|
|
|
const handlePaymentSuccess = async response => {
|
2025-04-02 17:47:30 -05:00
|
|
|
if (response && response?.preimage) {
|
|
|
|
const updated = await update();
|
2025-04-17 13:00:58 -05:00
|
|
|
showToast('success', 'Payment Success', 'You have successfully purchased this course');
|
2025-04-02 17:47:30 -05:00
|
|
|
} else {
|
2025-04-17 13:00:58 -05:00
|
|
|
showToast('error', 'Error', 'Failed to purchase course. Please try again.');
|
2024-11-20 17:46:52 -06:00
|
|
|
}
|
2025-04-02 17:47:30 -05:00
|
|
|
};
|
2024-11-20 17:46:52 -06:00
|
|
|
|
2025-04-17 13:00:58 -05:00
|
|
|
const handlePaymentError = error => {
|
2025-04-02 17:47:30 -05:00
|
|
|
showToast(
|
2025-04-17 13:00:58 -05:00
|
|
|
'error',
|
|
|
|
'Payment Error',
|
2025-04-02 17:47:30 -05:00
|
|
|
`Failed to purchase course. Please try again. Error: ${error}`
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2025-04-12 13:50:29 -05:00
|
|
|
const toggleTab = (index) => {
|
|
|
|
const tabMap = ['overview', 'content', 'qa'];
|
|
|
|
// If mobile and we have the lessons tab, insert it at index 2
|
|
|
|
if (isMobileView) {
|
|
|
|
tabMap.splice(2, 0, 'lessons');
|
|
|
|
}
|
|
|
|
|
|
|
|
const tabName = tabMap[index];
|
|
|
|
setActiveTab(tabName);
|
|
|
|
|
|
|
|
// Only show/hide sidebar on mobile - desktop keeps sidebar visible
|
|
|
|
if (isMobileView) {
|
|
|
|
if (tabName === 'lessons') {
|
|
|
|
setSidebarVisible(true);
|
|
|
|
} else {
|
|
|
|
setSidebarVisible(false);
|
|
|
|
}
|
2025-04-04 10:52:03 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-04-13 12:43:24 -05:00
|
|
|
const handleToggleSidebar = () => {
|
|
|
|
setSidebarVisible(!sidebarVisible);
|
|
|
|
};
|
|
|
|
|
2025-04-12 13:50:29 -05:00
|
|
|
// Map active tab name back to index for MenuTab
|
|
|
|
const getActiveTabIndex = () => {
|
|
|
|
const tabMap = ['overview', 'content', 'qa'];
|
|
|
|
if (isMobileView) {
|
|
|
|
tabMap.splice(2, 0, 'lessons');
|
|
|
|
}
|
|
|
|
|
|
|
|
return tabMap.indexOf(activeTab);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create tab items for MenuTab
|
|
|
|
const getTabItems = () => {
|
|
|
|
const items = [
|
|
|
|
{
|
2025-04-13 12:43:24 -05:00
|
|
|
label: 'Overview',
|
2025-04-12 13:50:29 -05:00
|
|
|
icon: 'pi pi-home',
|
|
|
|
},
|
|
|
|
{
|
2025-04-13 12:43:24 -05:00
|
|
|
label: 'Content',
|
2025-04-12 13:50:29 -05:00
|
|
|
icon: 'pi pi-book',
|
|
|
|
}
|
|
|
|
];
|
|
|
|
|
|
|
|
// Add lessons tab only on mobile
|
|
|
|
if (isMobileView) {
|
|
|
|
items.push({
|
2025-04-13 12:43:24 -05:00
|
|
|
label: 'Lessons',
|
2025-04-12 13:50:29 -05:00
|
|
|
icon: 'pi pi-list',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
items.push({
|
2025-04-13 14:36:55 -05:00
|
|
|
label: 'Comments',
|
2025-04-12 13:50:29 -05:00
|
|
|
icon: 'pi pi-comments',
|
|
|
|
});
|
|
|
|
|
|
|
|
return items;
|
|
|
|
};
|
|
|
|
|
2025-04-14 12:11:05 -05:00
|
|
|
// Add keyboard navigation support for tabs
|
|
|
|
useEffect(() => {
|
|
|
|
const handleKeyDown = (e) => {
|
|
|
|
if (e.key === 'ArrowRight') {
|
|
|
|
const currentIndex = getActiveTabIndex();
|
|
|
|
const nextIndex = (currentIndex + 1) % getTabItems().length;
|
|
|
|
toggleTab(nextIndex);
|
|
|
|
} else if (e.key === 'ArrowLeft') {
|
|
|
|
const currentIndex = getActiveTabIndex();
|
|
|
|
const prevIndex = (currentIndex - 1 + getTabItems().length) % getTabItems().length;
|
|
|
|
toggleTab(prevIndex);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
|
|
};
|
|
|
|
}, [activeTab]);
|
|
|
|
|
2025-04-12 13:50:29 -05:00
|
|
|
// Render the QA section (empty for now)
|
|
|
|
const renderQASection = () => {
|
|
|
|
return (
|
2025-04-13 17:04:31 -05:00
|
|
|
<div className="rounded-lg p-8 mt-4 bg-gray-800">
|
2025-04-12 13:50:29 -05:00
|
|
|
<h2 className="text-xl font-bold mb-4">Comments</h2>
|
2025-04-13 17:04:31 -05:00
|
|
|
{nAddress !== null ? (
|
|
|
|
<ZapThreadsWrapper
|
|
|
|
anchor={nAddress}
|
|
|
|
user={session?.user?.pubkey ? nip19.npubEncode(session?.user?.pubkey) : null}
|
|
|
|
relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.primal.net/, wss://nostrue.com/, wss://purplerelay.com/, wss://relay.devs.tools/"
|
|
|
|
disable="zaps"
|
|
|
|
/>
|
|
|
|
) : course?.d ? (
|
|
|
|
<ZapThreadsWrapper
|
|
|
|
anchor={course.d}
|
|
|
|
user={session?.user?.pubkey ? nip19.npubEncode(session?.user?.pubkey) : null}
|
|
|
|
relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.primal.net/, wss://nostrue.com/, wss://purplerelay.com/, wss://relay.devs.tools/"
|
|
|
|
disable="zaps"
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<p>Loading comments...</p>
|
|
|
|
)}
|
2025-04-12 13:50:29 -05:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
// Render Course Overview section
|
|
|
|
const renderOverviewSection = () => {
|
2025-04-13 12:43:24 -05:00
|
|
|
// Get isCompleted status for use in the component
|
|
|
|
const isCompleted = completedLessons.length > 0;
|
|
|
|
|
2025-04-12 13:50:29 -05:00
|
|
|
return (
|
2025-04-13 17:04:31 -05:00
|
|
|
<div className={`bg-gray-800 rounded-lg border border-gray-800 shadow-md ${isMobileView ? 'p-4' : 'p-6'}`}>
|
2025-04-13 12:43:24 -05:00
|
|
|
{isMobileView && course && (
|
|
|
|
<div className="mb-2">
|
|
|
|
{/* Completed tag above image in mobile view */}
|
|
|
|
{isCompleted && (
|
|
|
|
<div className="mb-2">
|
|
|
|
<Tag severity="success" value="Completed" />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Course image */}
|
|
|
|
{course.image && (
|
|
|
|
<div className="w-full h-48 relative rounded-lg overflow-hidden mb-3">
|
|
|
|
<img
|
|
|
|
src={course.image}
|
|
|
|
alt={course.title}
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
2025-04-12 13:50:29 -05:00
|
|
|
<CourseDetails
|
|
|
|
processedEvent={course}
|
|
|
|
paidCourse={paidCourse}
|
|
|
|
lessons={uniqueLessons}
|
|
|
|
decryptionPerformed={decryptionPerformed}
|
|
|
|
handlePaymentSuccess={handlePaymentSuccess}
|
|
|
|
handlePaymentError={handlePaymentError}
|
2025-04-13 12:43:24 -05:00
|
|
|
isMobileView={isMobileView}
|
|
|
|
showCompletedTag={!isMobileView}
|
2025-04-12 13:50:29 -05:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
if (courseLoading || decryptionLoading) {
|
2024-02-27 18:29:57 -06:00
|
|
|
return (
|
2025-04-02 17:47:30 -05:00
|
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
|
|
<ProgressSpinner />
|
|
|
|
</div>
|
2024-02-27 18:29:57 -06:00
|
|
|
);
|
2025-04-02 17:47:30 -05:00
|
|
|
}
|
|
|
|
|
2025-04-17 13:00:58 -05:00
|
|
|
const renderLesson = lesson => {
|
|
|
|
if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) {
|
2025-04-02 17:47:30 -05:00
|
|
|
return (
|
|
|
|
<CombinedLesson
|
|
|
|
lesson={lesson}
|
|
|
|
course={course}
|
|
|
|
decryptionPerformed={decryptionPerformed}
|
|
|
|
isPaid={paidCourse}
|
|
|
|
setCompleted={setCompleted}
|
|
|
|
/>
|
2025-04-13 20:41:21 -05:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
);
|
2025-04-17 13:00:58 -05:00
|
|
|
} else if (lesson.type === 'video' && !lesson.topics?.includes('document')) {
|
2025-04-02 17:47:30 -05:00
|
|
|
return (
|
|
|
|
<VideoLesson
|
|
|
|
lesson={lesson}
|
|
|
|
course={course}
|
|
|
|
decryptionPerformed={decryptionPerformed}
|
|
|
|
isPaid={paidCourse}
|
|
|
|
setCompleted={setCompleted}
|
|
|
|
/>
|
|
|
|
);
|
2025-04-17 13:00:58 -05:00
|
|
|
} else if (lesson.type === 'document' && !lesson.topics?.includes('video')) {
|
2025-04-02 17:47:30 -05:00
|
|
|
return (
|
|
|
|
<DocumentLesson
|
|
|
|
lesson={lesson}
|
|
|
|
course={course}
|
|
|
|
decryptionPerformed={decryptionPerformed}
|
|
|
|
isPaid={paidCourse}
|
|
|
|
setCompleted={setCompleted}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
2025-04-13 20:41:21 -05:00
|
|
|
<div className="mx-auto px-8 max-mob:px-0 mb-12 mt-4">
|
2025-04-12 13:50:29 -05:00
|
|
|
{/* Tab navigation using MenuTab component */}
|
2025-04-13 14:36:55 -05:00
|
|
|
<div className="sticky z-10 bg-transparent border-b border-gray-700/30"
|
|
|
|
style={{
|
|
|
|
top: `${navbarHeight}px`,
|
|
|
|
height: `${navbarHeight}px`
|
|
|
|
}}>
|
2025-04-12 13:50:29 -05:00
|
|
|
<MenuTab
|
|
|
|
items={getTabItems()}
|
|
|
|
activeIndex={getActiveTabIndex()}
|
|
|
|
onTabChange={(index) => toggleTab(index)}
|
2025-04-13 12:43:24 -05:00
|
|
|
sidebarVisible={sidebarVisible}
|
|
|
|
onToggleSidebar={handleToggleSidebar}
|
|
|
|
isMobileView={isMobileView}
|
2025-04-04 10:52:03 -05:00
|
|
|
/>
|
2025-04-12 13:50:29 -05:00
|
|
|
</div>
|
2025-04-04 10:52:03 -05:00
|
|
|
|
2025-04-13 12:43:24 -05:00
|
|
|
{/* Revised layout structure to prevent content flexing */}
|
|
|
|
<div className="relative mt-4">
|
|
|
|
{/* Main content area with fixed width */}
|
|
|
|
<div className={`transition-all duration-500 ease-in-out ${isMobileView ? 'w-full' : 'w-full'}`}
|
|
|
|
style={!isMobileView && sidebarVisible ? {paddingRight: '320px'} : {}}>
|
2025-04-12 13:50:29 -05:00
|
|
|
{/* Overview tab content */}
|
|
|
|
<div className={`${activeTab === 'overview' ? 'block' : 'hidden'}`}>
|
|
|
|
{renderOverviewSection()}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Content tab content */}
|
|
|
|
<div className={`${activeTab === 'content' ? 'block' : 'hidden'}`}>
|
|
|
|
{uniqueLessons.length > 0 && uniqueLessons[activeIndex] ? (
|
2025-04-14 11:45:09 -05:00
|
|
|
<div className="bg-gray-800 rounded-lg shadow-sm overflow-hidden">
|
2025-04-12 13:50:29 -05:00
|
|
|
{renderLesson(uniqueLessons[activeIndex])}
|
|
|
|
</div>
|
|
|
|
) : (
|
2025-04-14 11:45:09 -05:00
|
|
|
<div className="text-center bg-gray-800 rounded-lg p-8">
|
2025-04-12 13:50:29 -05:00
|
|
|
<p>Select a lesson from the sidebar to begin learning.</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{course?.content && (
|
2025-04-14 11:45:09 -05:00
|
|
|
<div className="mt-8 bg-gray-800 rounded-lg shadow-sm">
|
2025-04-13 20:41:21 -05:00
|
|
|
<MarkdownDisplay content={course.content} className="p-4 rounded-lg" />
|
2025-04-12 13:50:29 -05:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* QA tab content */}
|
|
|
|
<div className={`${activeTab === 'qa' ? 'block' : 'hidden'}`}>
|
|
|
|
{renderQASection()}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2025-04-13 12:43:24 -05:00
|
|
|
{/* 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] ${
|
2025-04-12 13:50:29 -05:00
|
|
|
(isMobileView && activeTab === 'lessons') ? 'ml-0 w-auto opacity-100' :
|
|
|
|
'w-0 ml-0 opacity-0 overflow-hidden'
|
2025-04-13 12:43:24 -05:00
|
|
|
}`}>
|
|
|
|
<CourseSidebar
|
|
|
|
lessons={uniqueLessons}
|
|
|
|
activeIndex={activeIndex}
|
|
|
|
onLessonSelect={(index) => {
|
|
|
|
handleLessonSelect(index);
|
|
|
|
if (isMobileView) {
|
2025-04-14 12:00:56 -05:00
|
|
|
toggleTab(getTabItems().findIndex(item => item.label === 'Content'));
|
2025-04-13 12:43:24 -05:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
completedLessons={completedLessons}
|
|
|
|
isMobileView={isMobileView}
|
|
|
|
onClose={() => {
|
|
|
|
setSidebarVisible(false);
|
|
|
|
setActiveTab('content');
|
|
|
|
}}
|
|
|
|
sidebarVisible={sidebarVisible}
|
|
|
|
setSidebarVisible={setSidebarVisible}
|
|
|
|
hideToggleButton={true}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
2025-04-04 10:52:03 -05:00
|
|
|
</div>
|
2025-04-02 17:47:30 -05:00
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
2024-02-27 18:29:57 -06:00
|
|
|
|
2024-10-14 18:58:12 -05:00
|
|
|
export default Course;
|