course view tabs for mobile view sidebar for full screen

This commit is contained in:
austinkelsay 2025-04-04 10:52:03 -05:00
parent 54ec3df1d7
commit a3e8cda6f4
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
3 changed files with 190 additions and 157 deletions

View File

@ -1,110 +1,111 @@
import React from 'react'; import React from 'react';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import Image from 'next/image'; import Image from 'next/image';
import { useImageProxy } from '@/hooks/useImageProxy'; import { useImageProxy } from '@/hooks/useImageProxy';
const CourseSidebar = ({ const CourseSidebar = ({
lessons, lessons,
activeIndex, activeIndex,
onLessonSelect, onLessonSelect,
completedLessons, completedLessons,
isMobileView, isMobileView,
onClose, onClose,
sidebarVisible sidebarVisible,
}) => { }) => {
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const LessonItem = ({ lesson, index }) => ( const LessonItem = ({ lesson, index }) => (
<li <li
className={` className={`
rounded-lg overflow-hidden transition-all duration-200 rounded-lg overflow-hidden transition-all duration-200
${activeIndex === index ${
? 'bg-blue-900/40 border-l-4 border-blue-500' activeIndex === index
: 'hover:bg-gray-700/50 active:bg-gray-700/80 border-l-4 border-transparent'} ? 'bg-blue-900/40 border-l-4 border-blue-500'
: 'hover:bg-gray-700/50 active:bg-gray-700/80 border-l-4 border-transparent'
}
${isMobileView ? 'mb-3' : 'mb-2'} ${isMobileView ? 'mb-3' : 'mb-2'}
`} `}
onClick={() => onLessonSelect(index)} onClick={() => onLessonSelect(index)}
> >
<div className={`flex items-start p-3 cursor-pointer ${isMobileView ? 'p-4' : 'p-3'}`}> <div className={`flex items-start p-3 cursor-pointer ${isMobileView ? 'p-4' : 'p-3'}`}>
{lesson.image && ( {lesson.image && (
<div className={`relative rounded-md overflow-hidden flex-shrink-0 mr-3 ${isMobileView ? 'w-16 h-16' : 'w-12 h-12'}`}> <div
<Image className={`relative rounded-md overflow-hidden flex-shrink-0 mr-3 ${isMobileView ? 'w-16 h-16' : 'w-12 h-12'}`}
src={returnImageProxy(lesson.image)} >
alt={`Lesson ${index + 1} thumbnail`} <Image
fill src={returnImageProxy(lesson.image)}
className="object-cover" alt={`Lesson ${index + 1} thumbnail`}
/> fill
</div> className="object-cover"
)} />
<div className="flex-1 min-w-0"> </div>
<div className="flex justify-between items-start w-full"> )}
<span className={`font-medium block mb-1 text-gray-300 ${isMobileView ? 'text-base' : 'text-sm'}`}> <div className="flex-1 min-w-0">
Lesson {index + 1} <div className="flex justify-between items-start w-full">
</span> <span
{completedLessons.includes(lesson.id) && ( className={`font-medium block mb-1 text-gray-300 ${isMobileView ? 'text-base' : 'text-sm'}`}
<Tag severity="success" value="Completed" className="ml-1 py-1 text-xs" /> >
)} Lesson {index + 1}
</div> </span>
<h3 className={`font-medium leading-tight line-clamp-2 text-[#f8f8ff] ${isMobileView ? 'text-base' : ''}`}> {completedLessons.includes(lesson.id) && (
{lesson.title} <Tag severity="success" value="Completed" className="ml-1 py-1 text-xs" />
</h3> )}
</div> </div>
</div> <h3
</li> className={`font-medium leading-tight line-clamp-2 text-[#f8f8ff] ${isMobileView ? 'text-base' : ''}`}
); >
{lesson.title}
</h3>
</div>
</div>
</li>
);
// For desktop sidebar // Desktop sidebar implementation
const DesktopSidebarContent = () => ( if (!isMobileView) {
return (
<div className="w-80 h-[calc(100vh-400px)] sticky top-8 overflow-hidden rounded-lg border border-gray-800 shadow-sm bg-gray-900">
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
<div className="flex flex-col p-4 h-full bg-gray-800 text-[#f8f8ff]"> <div className="flex flex-col p-4 h-full bg-gray-800 text-[#f8f8ff]">
<div className="flex items-center justify-between border-b border-gray-700 pb-4 mb-4"> <div className="flex items-center justify-between border-b border-gray-700 pb-4 mb-4">
<h2 className="font-bold text-white text-lg">Course Lessons</h2> <h2 className="font-bold text-white text-lg">Course Lessons</h2>
</div>
<div className="overflow-y-auto flex-1 pb-16">
<ul className="space-y-2">
{lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} index={index} />
))}
</ul>
</div>
</div> </div>
</div> <div className="overflow-y-auto flex-1">
); <ul className="space-y-2">
// For mobile sidebar
const MobileSidebarContent = () => (
<div className="bg-gray-800 text-[#f8f8ff] p-4">
<div className="border-b border-gray-700 pb-4 mb-4">
<h2 className="font-bold text-white text-xl">Course Lessons</h2>
</div>
<ul className="space-y-0">
{lessons.map((lesson, index) => ( {lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} index={index} /> <LessonItem key={index} lesson={lesson} index={index} />
))} ))}
</ul> </ul>
</div>
</div>
</div> </div>
</div>
); );
}
// Desktop sidebar // Mobile sidebar implementation - completely restructured for better scrolling
if (!isMobileView) { if (isMobileView && sidebarVisible) {
return ( return (
<div className="w-80 h-[calc(100vh-400px)] sticky top-8 overflow-hidden rounded-lg border border-gray-800 shadow-sm bg-gray-900"> <div className="w-full bg-gray-900 rounded-lg border border-gray-800 shadow-md overflow-hidden mb-4">
<DesktopSidebarContent /> <div className="bg-gray-800 p-4 border-b border-gray-700">
</div> <h2 className="font-bold text-white text-xl">Course Lessons</h2>
); </div>
}
{/* Scrollable container with fixed height */}
<div className="overflow-y-scroll" style={{ maxHeight: '60vh', WebkitOverflowScrolling: 'touch' }}>
<div className="p-4 bg-gray-900">
<ul>
{lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} index={index} />
))}
</ul>
</div>
</div>
</div>
);
}
// Mobile sidebar - now integrated with tab system return null;
if (isMobileView && sidebarVisible) {
return (
<div className="w-full bg-gray-900 rounded-lg border border-gray-800 shadow-sm overflow-visible mb-4">
<MobileSidebarContent />
</div>
);
}
return null;
}; };
export default CourseSidebar; export default CourseSidebar;

View File

@ -12,6 +12,7 @@ const appConfig = {
authorPubkeys: [ authorPubkeys: [
'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741', 'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741',
'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345', 'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345',
'6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4',
], ],
customLightningAddresses: [ customLightningAddresses: [
{ {

View File

@ -5,17 +5,17 @@ import CourseDetails from '@/components/content/courses/CourseDetails';
import VideoLesson from '@/components/content/courses/VideoLesson'; import VideoLesson from '@/components/content/courses/VideoLesson';
import DocumentLesson from '@/components/content/courses/DocumentLesson'; import DocumentLesson from '@/components/content/courses/DocumentLesson';
import CombinedLesson from '@/components/content/courses/CombinedLesson'; import CombinedLesson from '@/components/content/courses/CombinedLesson';
import CourseSidebar from '@/components/content/courses/CourseSidebar';
import { useNDKContext } from '@/context/NDKContext'; import { useNDKContext } from '@/context/NDKContext';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { ProgressSpinner } from 'primereact/progressspinner'; import { ProgressSpinner } from 'primereact/progressspinner';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { Tag } from 'primereact/tag';
import { useDecryptContent } from '@/hooks/encryption/useDecryptContent'; import { useDecryptContent } from '@/hooks/encryption/useDecryptContent';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
import appConfig from '@/config/appConfig'; import appConfig from '@/config/appConfig';
import useWindowWidth from '@/hooks/useWindowWidth';
const MDDisplay = dynamic(() => import('@uiw/react-markdown-preview'), { const MDDisplay = dynamic(() => import('@uiw/react-markdown-preview'), {
ssr: false, ssr: false,
@ -176,11 +176,15 @@ const Course = () => {
const { ndk, addSigner } = useNDKContext(); const { ndk, addSigner } = useNDKContext();
const { data: session, update } = useSession(); const { data: session, update } = useSession();
const { showToast } = useToast(); const { showToast } = useToast();
const [expandedIndex, setExpandedIndex] = useState(null); const [activeIndex, setActiveIndex] = useState(0);
const [completedLessons, setCompletedLessons] = useState([]); const [completedLessons, setCompletedLessons] = useState([]);
const [nAddresses, setNAddresses] = useState({}); const [nAddresses, setNAddresses] = useState({});
const [nsec, setNsec] = useState(null); const [nsec, setNsec] = useState(null);
const [npub, setNpub] = useState(null); const [npub, setNpub] = useState(null);
const [sidebarVisible, setSidebarVisible] = useState(false);
const windowWidth = useWindowWidth();
const isMobileView = windowWidth <= 968;
const [activeTab, setActiveTab] = useState('content'); // Default to content tab on mobile
const setCompleted = useCallback(lessonId => { const setCompleted = useCallback(lessonId => {
setCompletedLessons(prev => [...prev, lessonId]); setCompletedLessons(prev => [...prev, lessonId]);
@ -220,12 +224,20 @@ const Course = () => {
if (router.isReady) { if (router.isReady) {
const { active } = router.query; const { active } = router.query;
if (active !== undefined) { if (active !== undefined) {
setExpandedIndex(parseInt(active, 10)); setActiveIndex(parseInt(active, 10));
} else { } else {
setExpandedIndex(null); setActiveIndex(0);
}
// Auto-open sidebar on desktop, close on mobile
setSidebarVisible(!isMobileView);
// Reset to content tab when switching to mobile
if (isMobileView) {
setActiveTab('content');
} }
} }
}, [router.isReady, router.query]); }, [router.isReady, router.query, isMobileView]);
useEffect(() => { useEffect(() => {
if (uniqueLessons.length > 0) { if (uniqueLessons.length > 0) {
@ -256,15 +268,15 @@ const Course = () => {
setNpub(null); setNpub(null);
} }
}, [session]); }, [session]);
const handleLessonSelect = index => {
setActiveIndex(index);
router.push(`/course/${router.query.slug}?active=${index}`, undefined, { shallow: true });
const handleAccordionChange = e => { // On mobile, switch to content tab after selection
const newIndex = e.index === expandedIndex ? null : e.index; if (isMobileView) {
setExpandedIndex(newIndex); setActiveTab('content');
setSidebarVisible(false);
if (newIndex !== null) {
router.push(`/course/${router.query.slug}?active=${newIndex}`, undefined, { shallow: true });
} else {
router.push(`/course/${router.query.slug}`, undefined, { shallow: true });
} }
}; };
@ -285,6 +297,15 @@ const Course = () => {
); );
}; };
const toggleTab = tab => {
setActiveTab(tab);
if (tab === 'lessons') {
setSidebarVisible(true);
} else {
setSidebarVisible(false);
}
};
if (courseLoading || decryptionLoading) { if (courseLoading || decryptionLoading) {
return ( return (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
@ -339,62 +360,72 @@ const Course = () => {
handlePaymentError={handlePaymentError} handlePaymentError={handlePaymentError}
/> />
)} )}
<Accordion
activeIndex={expandedIndex} <div className="mx-4">
onTabChange={handleAccordionChange} {/* Mobile tab navigation */}
className="mt-4 px-4 max-mob:px-0 max-tab:px-0" {isMobileView && (
> <div className="flex w-full border-b border-gray-200 dark:border-gray-700 mb-4">
{uniqueLessons.length > 0 && <button
uniqueLessons.map((lesson, index) => ( className={`flex-1 py-3 font-medium text-center border-b-2 ${
<AccordionTab activeTab === 'lessons'
key={index} ? 'border-blue-500 text-blue-600 dark:text-blue-400'
pt={{ : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
root: { className: 'border-none' }, }`}
header: { className: 'border-none' }, onClick={() => toggleTab('lessons')}
headerAction: { className: 'border-none' },
content: { className: 'border-none max-mob:px-0 max-tab:px-0' },
accordiontab: { className: 'border-none' },
}}
header={
<div className="flex align-items-center justify-between w-full">
<span
id={`lesson-${index}`}
className="font-bold text-xl"
>{`Lesson ${index + 1}: ${lesson.title}`}</span>
{completedLessons.includes(lesson.id) ? (
<Tag severity="success" value="Completed" />
) : null}
</div>
}
> >
<div className="w-full py-4 rounded-b-lg"> Course Lessons
{renderLesson(lesson)} </button>
{nAddresses[lesson.id] && ( <button
<div className="mt-8"> className={`flex-1 py-3 font-medium text-center border-b-2 ${
{!paidCourse || decryptionPerformed || session?.user?.role?.subscribed ? ( activeTab === 'content'
<ZapThreadsWrapper ? 'border-blue-500 text-blue-600 dark:text-blue-400'
anchor={nAddresses[lesson.id]} : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
user={session?.user ? nsec || npub : null} }`}
relays={appConfig.defaultRelayUrls.join(',')} onClick={() => toggleTab('content')}
disable="zaps" >
isAuthorized={true} Lesson Content
/> </button>
) : ( </div>
<div className="text-center p-4 bg-gray-800/50 rounded-lg"> )}
<p className="text-gray-400">
Comments are only available to course purchasers, subscribers, and the <div className="flex relative">
course creator. {/* Course Sidebar Component */}
</p> <CourseSidebar
</div> lessons={uniqueLessons}
)} activeIndex={activeIndex}
</div> onLessonSelect={handleLessonSelect}
)} completedLessons={completedLessons}
isMobileView={isMobileView}
onClose={() => {
setSidebarVisible(false);
if (isMobileView) setActiveTab('content');
}}
sidebarVisible={sidebarVisible}
/>
{/* Main content */}
<div
className={`transition-all duration-200 ${
!isMobileView ? 'ml-8 flex-1' : activeTab === 'content' ? 'w-full' : 'w-full hidden'
}`}
>
{uniqueLessons.length > 0 && uniqueLessons[activeIndex] ? (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
{renderLesson(uniqueLessons[activeIndex])}
</div> </div>
</AccordionTab> ) : (
))} <div className="text-center bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-8">
</Accordion> <p>Select a lesson from the sidebar to begin learning.</p>
<div className="mx-auto my-6"> </div>
{course?.content && <MDDisplay className="p-4 rounded-lg" source={course.content} />} )}
{course?.content && (
<div className="mt-8 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
<MDDisplay className="p-4 rounded-lg" source={course.content} />
</div>
)}
</div>
</div>
</div> </div>
</> </>
); );