improve course page architecture with custom hooks and components

This commit is contained in:
austinkelsay 2025-05-11 14:07:55 -05:00
parent 51cd1e4d97
commit 027bf28e2f
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
20 changed files with 384 additions and 390 deletions

View File

@ -19,7 +19,7 @@ import { ProgressSpinner } from 'primereact/progressspinner';
import { Toast } from 'primereact/toast';
// Import the desktop and mobile components
import DesktopCourseDetails from './DesktopCourseDetails';
import DesktopCourseDetails from '@/components/content/courses/details/DesktopCourseDetails';
import MobileCourseDetails from './MobileCourseDetails';
export default function CourseDetails({

View File

@ -0,0 +1,82 @@
import React from 'react';
import VideoLesson from '@/components/content/courses/lessons/VideoLesson';
import DocumentLesson from '@/components/content/courses/lessons/DocumentLesson';
import CombinedLesson from '@/components/content/courses/lessons/CombinedLesson';
import MarkdownDisplay from '@/components/markdown/MarkdownDisplay';
/**
* Component to display course content including lessons
*/
const CourseContent = ({
lessons,
activeIndex,
course,
paidCourse,
decryptedLessonIds,
setCompleted
}) => {
const renderLesson = (lesson) => {
if (!lesson) return null;
// Check if this specific lesson is decrypted
const lessonDecrypted = !paidCourse || decryptedLessonIds[lesson.id] || false;
if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) {
return (
<CombinedLesson
lesson={lesson}
course={course}
decryptionPerformed={lessonDecrypted}
isPaid={paidCourse}
setCompleted={setCompleted}
/>
);
} else if (lesson.type === 'video' && !lesson.topics?.includes('document')) {
return (
<VideoLesson
lesson={lesson}
course={course}
decryptionPerformed={lessonDecrypted}
isPaid={paidCourse}
setCompleted={setCompleted}
/>
);
} else if (lesson.type === 'document' && !lesson.topics?.includes('video')) {
return (
<DocumentLesson
lesson={lesson}
course={course}
decryptionPerformed={lessonDecrypted}
isPaid={paidCourse}
setCompleted={setCompleted}
/>
);
}
return null;
};
return (
<>
{lessons.length > 0 && lessons[activeIndex] ? (
<div className="bg-gray-800 rounded-lg shadow-sm overflow-hidden">
<div key={`lesson-${lessons[activeIndex].id}`}>
{renderLesson(lessons[activeIndex])}
</div>
</div>
) : (
<div className="text-center bg-gray-800 rounded-lg p-8">
<p>Select a lesson from the sidebar to begin learning.</p>
</div>
)}
{course?.content && (
<div className="mt-8 bg-gray-800 rounded-lg shadow-sm">
<MarkdownDisplay content={course.content} className="p-4 rounded-lg" />
</div>
)}
</>
);
};
export default CourseContent;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Tag } from 'primereact/tag';
import CourseDetails from '../details/CourseDetails';
/**
* Component to display course overview with details
*/
const CourseOverview = ({
course,
paidCourse,
lessons,
decryptionPerformed,
handlePaymentSuccess,
handlePaymentError,
isMobileView,
completedLessons
}) => {
// Determine if course is completed
const isCompleted = completedLessons.length > 0;
return (
<div className={`bg-gray-800 rounded-lg border border-gray-800 shadow-md ${isMobileView ? 'p-4' : 'p-6'}`}>
{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>
)}
<CourseDetails
processedEvent={course}
paidCourse={paidCourse}
lessons={lessons}
decryptionPerformed={decryptionPerformed}
handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError}
isMobileView={isMobileView}
showCompletedTag={!isMobileView}
/>
</div>
);
};
export default CourseOverview;

View File

@ -0,0 +1,32 @@
import React from 'react';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
/**
* Component to display course comments and Q&A section
*/
const CourseQA = ({ nAddress, isAuthorized, nsec, npub }) => {
return (
<div className="rounded-lg p-8 mt-4 bg-gray-800 max-mob:px-4">
<h2 className="text-xl font-bold mb-4">Comments</h2>
{nAddress !== null && isAuthorized ? (
<div className="px-4 max-mob:px-0">
<ZapThreadsWrapper
anchor={nAddress}
user={nsec || npub || 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"
isAuthorized={isAuthorized}
/>
</div>
) : (
<div className="text-center p-4 mx-4 bg-gray-800/50 rounded-lg">
<p className="text-gray-400">
Comments are only available to content purchasers, subscribers, and the content creator.
</p>
</div>
)}
</div>
);
};
export default CourseQA;

View File

@ -8,7 +8,7 @@ import { useSession } from 'next-auth/react';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';
import useWindowWidth from '@/hooks/useWindowWidth';
import CourseHeader from '../content/courses/CourseHeader';
import CourseHeader from '../content/courses/layout/CourseHeader';
import { useNDKContext } from '@/context/NDKContext';
import { nip19 } from 'nostr-tools';
import { parseCourseEvent } from '@/utils/nostr';

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { parseCourseEvent } from '@/utils/nostr';
import { useState, useEffect } from 'react';
import { nip19 } from 'nostr-tools';
import { useToast } from '../useToast';
import { parseCourseEvent } from '@/utils/nostr';
import { useToast } from '@/hooks/useToast';
/**
* Hook to fetch and manage course data
@ -21,7 +21,8 @@ const useCourseData = (ndk, fetchAuthor, router) => {
if (!router.isReady) return;
const { slug } = router.query;
let id;
const fetchCourseId = async () => {
if (slug.includes('naddr')) {
const { data } = nip19.decode(slug);
@ -54,7 +55,7 @@ const useCourseData = (ndk, fetchAuthor, router) => {
const initializeCourse = async () => {
setLoading(true);
const id = await fetchCourseId();
id = await fetchCourseId();
if (!id) {
setLoading(false);
return;

View File

@ -0,0 +1,142 @@
import { useState, useEffect, useMemo } from 'react';
/**
* Hook to manage course navigation and tab logic
* @param {Object} router - Next.js router instance
* @param {Boolean} isMobileView - Whether the current view is mobile
* @returns {Object} Navigation state and functions
*/
const useCourseNavigation = (router, isMobileView) => {
const [activeIndex, setActiveIndex] = useState(0);
const [sidebarVisible, setSidebarVisible] = useState(false);
const [activeTab, setActiveTab] = useState('overview'); // Default to overview tab
// Memoized function to get the tab map based on view mode
const tabMap = useMemo(() => {
const baseTabMap = ['overview', 'content', 'qa'];
if (isMobileView) {
const mobileTabMap = [...baseTabMap];
mobileTabMap.splice(2, 0, 'lessons');
return mobileTabMap;
}
return baseTabMap;
}, [isMobileView]);
// Initialize navigation state based on router
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]);
// Function to handle lesson selection
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);
}
};
// Function to toggle tab
const toggleTab = (index) => {
const tabName = tabMap[index];
setActiveTab(tabName);
// Only show/hide sidebar on mobile - desktop keeps sidebar visible
if (isMobileView) {
setSidebarVisible(tabName === 'lessons');
}
};
// Function to toggle sidebar visibility
const toggleSidebar = () => {
setSidebarVisible(!sidebarVisible);
};
// Map active tab name back to index for MenuTab
const getActiveTabIndex = () => {
return tabMap.indexOf(activeTab);
};
// Create tab items for MenuTab
const getTabItems = () => {
const items = [
{
label: 'Overview',
icon: 'pi pi-home',
},
{
label: 'Content',
icon: 'pi pi-book',
}
];
// Add lessons tab only on mobile
if (isMobileView) {
items.push({
label: 'Lessons',
icon: 'pi pi-list',
});
}
items.push({
label: 'Comments',
icon: 'pi pi-comments',
});
return items;
};
// Add keyboard navigation support for tabs
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight') {
const currentIndex = getActiveTabIndex();
const nextIndex = (currentIndex + 1) % tabMap.length;
toggleTab(nextIndex);
} else if (e.key === 'ArrowLeft') {
const currentIndex = getActiveTabIndex();
const prevIndex = (currentIndex - 1 + tabMap.length) % tabMap.length;
toggleTab(prevIndex);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [activeTab, tabMap]);
return {
activeIndex,
setActiveIndex,
activeTab,
setActiveTab,
sidebarVisible,
setSidebarVisible,
handleLessonSelect,
toggleTab,
toggleSidebar,
getActiveTabIndex,
getTabItems,
tabMap
};
};
export default useCourseNavigation;

View File

@ -30,7 +30,7 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
const events = await ndk.fetchEvents(filter);
const newLessons = [];
// Process events
// Process events (no need to check for duplicates here)
for (const event of events) {
const author = await fetchAuthor(event.pubkey);
const parsedLesson = { ...parseEvent(event), author };
@ -47,7 +47,7 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
}
}, [lessonIds, ndk, fetchAuthor, pubkey]);
// Deduplicate lessons
// Keep this deduplication logic using Map
useEffect(() => {
const newUniqueLessons = Array.from(
new Map(lessons.map(lesson => [lesson.id, lesson])).values()

View File

@ -2,8 +2,8 @@ import React, { useEffect, useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';
import { parseEvent, findKind0Fields } from '@/utils/nostr';
import DraftCourseDetails from '@/components/content/courses/DraftCourseDetails';
import DraftCourseLesson from '@/components/content/courses/DraftCourseLesson';
import DraftCourseDetails from '@/components/content/courses/details/DraftCourseDetails';
import DraftCourseLesson from '@/components/content/courses/details/DraftCourseLesson';
import { useNDKContext } from '@/context/NDKContext';
import { useSession } from 'next-auth/react';
import { useIsAdmin } from '@/hooks/useIsAdmin';

View File

@ -1,168 +1,55 @@
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
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';
import CourseSidebar from '@/components/content/courses/CourseSidebar';
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';
import { useDecryptContent } from '@/hooks/encryption/useDecryptContent';
// Hooks
import useCourseDecryption from '@/hooks/encryption/useCourseDecryption';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
import appConfig from '@/config/appConfig';
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';
import { Tag } from 'primereact/tag';
import MarkdownDisplay from '@/components/markdown/MarkdownDisplay';
const useCourseData = (ndk, fetchAuthor, router) => {
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 () => {
if (slug.includes('naddr')) {
const { data } = nip19.decode(slug);
if (!data?.identifier) {
showToast('error', 'Error', 'Resource not found');
return null;
}
return data.identifier;
} else {
return slug;
}
};
const fetchCourse = async courseId => {
try {
await ndk.connect();
const event = await ndk.fetchEvent({ '#d': [courseId] });
if (!event) return null;
const author = await fetchAuthor(event.pubkey);
const lessonIds = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1].split(':')[2]);
const parsedCourse = { ...parseCourseEvent(event), author };
return { parsedCourse, lessonIds };
} catch (error) {
console.error('Error fetching event:', error);
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 };
};
const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
const [lessons, setLessons] = useState([]);
const [uniqueLessons, setUniqueLessons] = useState([]);
const { showToast } = useToast();
useEffect(() => {
if (lessonIds.length > 0 && pubkey) {
const fetchLessons = async () => {
try {
await ndk.connect();
// Create a single filter with all lesson IDs to avoid multiple calls
const filter = {
'#d': lessonIds,
kinds: [30023, 30402],
authors: [pubkey],
};
const events = await ndk.fetchEvents(filter);
const newLessons = [];
// Process events (no need to check for duplicates here)
for (const event of events) {
const author = await fetchAuthor(event.pubkey);
const parsedLesson = { ...parseEvent(event), author };
newLessons.push(parsedLesson);
}
setLessons(newLessons);
} catch (error) {
console.error('Error fetching events:', error);
}
};
fetchLessons();
}
}, [lessonIds, ndk, fetchAuthor, pubkey]);
// Keep this deduplication logic using Map
useEffect(() => {
const newUniqueLessons = Array.from(
new Map(lessons.map(lesson => [lesson.id, lesson])).values()
);
setUniqueLessons(newUniqueLessons);
}, [lessons]);
return { lessons, uniqueLessons, setLessons };
};
// Config
import appConfig from '@/config/appConfig';
const Course = () => {
const router = useRouter();
const { ndk, addSigner } = useNDKContext();
const { data: session, update } = useSession();
const { showToast } = useToast();
const [activeIndex, setActiveIndex] = useState(0);
const [completedLessons, setCompletedLessons] = useState([]);
const [nAddresses, setNAddresses] = useState({});
const [nsec, setNsec] = useState(null);
const [npub, setNpub] = useState(null);
const [sidebarVisible, setSidebarVisible] = useState(false);
const [nAddress, setNAddress] = useState(null);
const windowWidth = useWindowWidth();
const isMobileView = windowWidth <= 968;
const [activeTab, setActiveTab] = useState('overview'); // Default to overview tab
const navbarHeight = 60; // Match the height from Navbar component
// Memoized function to get the tab map based on view mode
const getTabMap = useMemo(() => {
const baseTabMap = ['overview', 'content', 'qa'];
if (isMobileView) {
const mobileTabMap = [...baseTabMap];
mobileTabMap.splice(2, 0, 'lessons');
return mobileTabMap;
}
return baseTabMap;
}, [isMobileView]);
// Use our navigation hook
const {
activeIndex,
activeTab,
sidebarVisible,
setSidebarVisible,
handleLessonSelect,
toggleTab,
toggleSidebar,
getActiveTabIndex,
getTabItems,
} = useCourseNavigation(router, isMobileView);
useEffect(() => {
if (router.isReady && router.query.slug) {
@ -179,24 +66,6 @@ const Course = () => {
}
}, [router.isReady, router.query.slug, showToast, router]);
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]);
const setCompleted = useCallback(lessonId => {
setCompletedLessons(prev => [...prev, lessonId]);
}, []);
@ -268,18 +137,7 @@ const Course = () => {
session?.user?.role?.subscribed ||
session?.user?.pubkey === course?.pubkey ||
!paidCourse ||
session?.user?.purchased?.some(purchase => purchase.courseId === course?.d)
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);
}
};
session?.user?.purchased?.some(purchase => purchase.courseId === course?.d);
const handlePaymentSuccess = async response => {
if (response && response?.preimage) {
@ -298,141 +156,6 @@ const Course = () => {
);
};
const toggleTab = (index) => {
const tabName = getTabMap[index];
setActiveTab(tabName);
// Only show/hide sidebar on mobile - desktop keeps sidebar visible
if (isMobileView) {
setSidebarVisible(tabName === 'lessons');
}
};
const handleToggleSidebar = () => {
setSidebarVisible(!sidebarVisible);
};
// Map active tab name back to index for MenuTab
const getActiveTabIndex = () => {
return getTabMap.indexOf(activeTab);
};
// Create tab items for MenuTab
const getTabItems = () => {
const items = [
{
label: 'Overview',
icon: 'pi pi-home',
},
{
label: 'Content',
icon: 'pi pi-book',
}
];
// Add lessons tab only on mobile
if (isMobileView) {
items.push({
label: 'Lessons',
icon: 'pi pi-list',
});
}
items.push({
label: 'Comments',
icon: 'pi pi-comments',
});
return items;
};
// Add keyboard navigation support for tabs
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight') {
const currentIndex = getActiveTabIndex();
const nextIndex = (currentIndex + 1) % getTabMap.length;
toggleTab(nextIndex);
} else if (e.key === 'ArrowLeft') {
const currentIndex = getActiveTabIndex();
const prevIndex = (currentIndex - 1 + getTabMap.length) % getTabMap.length;
toggleTab(prevIndex);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [activeTab, getTabMap, toggleTab]);
// Render the QA section (empty for now)
const renderQASection = () => {
return (
<div className="rounded-lg p-8 mt-4 bg-gray-800 max-mob:px-4">
<h2 className="text-xl font-bold mb-4">Comments</h2>
{nAddress !== null && isAuthorized ? (
<div className="px-4 max-mob:px-0">
<ZapThreadsWrapper
anchor={nAddress}
user={nsec || npub || 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"
isAuthorized={isAuthorized}
/>
</div>
) : (
<div className="text-center p-4 mx-4 bg-gray-800/50 rounded-lg">
<p className="text-gray-400">
Comments are only available to content purchasers, subscribers, and the content creator.
</p>
</div>
)}
</div>
);
};
// Render Course Overview section
const renderOverviewSection = () => {
// Get isCompleted status for use in the component
const isCompleted = completedLessons.length > 0;
return (
<div className={`bg-gray-800 rounded-lg border border-gray-800 shadow-md ${isMobileView ? 'p-4' : 'p-6'}`}>
{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>
)}
<CourseDetails
processedEvent={course}
paidCourse={paidCourse}
lessons={uniqueLessons}
decryptionPerformed={decryptionPerformed}
handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError}
isMobileView={isMobileView}
showCompletedTag={!isMobileView}
/>
</div>
);
};
if (courseLoading || decryptionLoading) {
return (
<div className="w-full h-full flex items-center justify-center">
@ -441,45 +164,6 @@ const Course = () => {
);
}
const renderLesson = lesson => {
if (!lesson) return null;
// Check if this specific lesson is decrypted
const lessonDecrypted = !paidCourse || decryptedLessonIds[lesson.id] || false;
if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) {
return (
<CombinedLesson
lesson={lesson}
course={course}
decryptionPerformed={lessonDecrypted}
isPaid={paidCourse}
setCompleted={setCompleted}
/>
);
} else if (lesson.type === 'video' && !lesson.topics?.includes('document')) {
return (
<VideoLesson
lesson={lesson}
course={course}
decryptionPerformed={lessonDecrypted}
isPaid={paidCourse}
setCompleted={setCompleted}
/>
);
} else if (lesson.type === 'document' && !lesson.topics?.includes('video')) {
return (
<DocumentLesson
lesson={lesson}
course={course}
decryptionPerformed={lessonDecrypted}
isPaid={paidCourse}
setCompleted={setCompleted}
/>
);
}
};
return (
<>
<div className="mx-auto px-8 max-mob:px-0 mb-12 mt-2">
@ -494,45 +178,50 @@ const Course = () => {
activeIndex={getActiveTabIndex()}
onTabChange={(index) => toggleTab(index)}
sidebarVisible={sidebarVisible}
onToggleSidebar={handleToggleSidebar}
onToggleSidebar={toggleSidebar}
isMobileView={isMobileView}
/>
</div>
{/* Revised layout structure to prevent content flexing */}
{/* Main content area with fixed width */}
<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'} : {}}>
{/* Overview tab content */}
<div className={`${activeTab === 'overview' ? 'block' : 'hidden'}`}>
{renderOverviewSection()}
<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'}`}>
{uniqueLessons.length > 0 && uniqueLessons[activeIndex] ? (
<div className="bg-gray-800 rounded-lg shadow-sm overflow-hidden">
<div key={`lesson-${uniqueLessons[activeIndex].id}`}>
{renderLesson(uniqueLessons[activeIndex])}
</div>
</div>
) : (
<div className="text-center bg-gray-800 rounded-lg p-8">
<p>Select a lesson from the sidebar to begin learning.</p>
</div>
)}
{course?.content && (
<div className="mt-8 bg-gray-800 rounded-lg shadow-sm">
<MarkdownDisplay content={course.content} className="p-4 rounded-lg" />
</div>
)}
<CourseContent
lessons={uniqueLessons}
activeIndex={activeIndex}
course={course}
paidCourse={paidCourse}
decryptedLessonIds={decryptedLessonIds}
setCompleted={setCompleted}
/>
</div>
{/* QA tab content */}
<div className={`${activeTab === 'qa' ? 'block' : 'hidden'}`}>
{renderQASection()}
<CourseQA
nAddress={nAddress}
isAuthorized={isAuthorized}
nsec={nsec}
npub={npub}
/>
</div>
</div>
@ -556,12 +245,7 @@ const Course = () => {
<CourseSidebar
lessons={uniqueLessons}
activeIndex={activeIndex}
onLessonSelect={(index) => {
handleLessonSelect(index);
if (isMobileView) {
setActiveTab('content'); // Use the tab name directly
}
}}
onLessonSelect={handleLessonSelect}
completedLessons={completedLessons}
isMobileView={isMobileView}
sidebarVisible={sidebarVisible}
@ -577,17 +261,12 @@ const Course = () => {
<CourseSidebar
lessons={uniqueLessons}
activeIndex={activeIndex}
onLessonSelect={(index) => {
handleLessonSelect(index);
if (isMobileView) {
setActiveTab('content'); // Use the tab name directly
}
}}
onLessonSelect={handleLessonSelect}
completedLessons={completedLessons}
isMobileView={isMobileView}
onClose={() => {
setSidebarVisible(false);
setActiveTab('content');
toggleTab(getActiveTabIndex());
}}
sidebarVisible={sidebarVisible}
setSidebarVisible={setSidebarVisible}