mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-03 07:42:03 +00:00
course view tabs for mobile view sidebar for full screen
This commit is contained in:
parent
54ec3df1d7
commit
a3e8cda6f4
@ -1,110 +1,111 @@
|
||||
import React from 'react';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Button } from 'primereact/button';
|
||||
import Image from 'next/image';
|
||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||
|
||||
const CourseSidebar = ({
|
||||
lessons,
|
||||
activeIndex,
|
||||
onLessonSelect,
|
||||
completedLessons,
|
||||
isMobileView,
|
||||
onClose,
|
||||
sidebarVisible
|
||||
const CourseSidebar = ({
|
||||
lessons,
|
||||
activeIndex,
|
||||
onLessonSelect,
|
||||
completedLessons,
|
||||
isMobileView,
|
||||
onClose,
|
||||
sidebarVisible,
|
||||
}) => {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
const LessonItem = ({ lesson, index }) => (
|
||||
<li
|
||||
className={`
|
||||
const LessonItem = ({ lesson, index }) => (
|
||||
<li
|
||||
className={`
|
||||
rounded-lg overflow-hidden transition-all duration-200
|
||||
${activeIndex === index
|
||||
? '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'}
|
||||
${
|
||||
activeIndex === index
|
||||
? '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'}
|
||||
`}
|
||||
onClick={() => onLessonSelect(index)}
|
||||
>
|
||||
<div className={`flex items-start p-3 cursor-pointer ${isMobileView ? 'p-4' : 'p-3'}`}>
|
||||
{lesson.image && (
|
||||
<div className={`relative rounded-md overflow-hidden flex-shrink-0 mr-3 ${isMobileView ? 'w-16 h-16' : 'w-12 h-12'}`}>
|
||||
<Image
|
||||
src={returnImageProxy(lesson.image)}
|
||||
alt={`Lesson ${index + 1} thumbnail`}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<span className={`font-medium block mb-1 text-gray-300 ${isMobileView ? 'text-base' : 'text-sm'}`}>
|
||||
Lesson {index + 1}
|
||||
</span>
|
||||
{completedLessons.includes(lesson.id) && (
|
||||
<Tag severity="success" value="Completed" className="ml-1 py-1 text-xs" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className={`font-medium leading-tight line-clamp-2 text-[#f8f8ff] ${isMobileView ? 'text-base' : ''}`}>
|
||||
{lesson.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
onClick={() => onLessonSelect(index)}
|
||||
>
|
||||
<div className={`flex items-start p-3 cursor-pointer ${isMobileView ? 'p-4' : 'p-3'}`}>
|
||||
{lesson.image && (
|
||||
<div
|
||||
className={`relative rounded-md overflow-hidden flex-shrink-0 mr-3 ${isMobileView ? 'w-16 h-16' : 'w-12 h-12'}`}
|
||||
>
|
||||
<Image
|
||||
src={returnImageProxy(lesson.image)}
|
||||
alt={`Lesson ${index + 1} thumbnail`}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<span
|
||||
className={`font-medium block mb-1 text-gray-300 ${isMobileView ? 'text-base' : 'text-sm'}`}
|
||||
>
|
||||
Lesson {index + 1}
|
||||
</span>
|
||||
{completedLessons.includes(lesson.id) && (
|
||||
<Tag severity="success" value="Completed" className="ml-1 py-1 text-xs" />
|
||||
)}
|
||||
</div>
|
||||
<h3
|
||||
className={`font-medium leading-tight line-clamp-2 text-[#f8f8ff] ${isMobileView ? 'text-base' : ''}`}
|
||||
>
|
||||
{lesson.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
// For desktop sidebar
|
||||
const DesktopSidebarContent = () => (
|
||||
// Desktop sidebar implementation
|
||||
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="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">
|
||||
<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 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">
|
||||
<h2 className="font-bold text-white text-lg">Course Lessons</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 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">
|
||||
<div className="overflow-y-auto flex-1">
|
||||
<ul className="space-y-2">
|
||||
{lessons.map((lesson, index) => (
|
||||
<LessonItem key={index} lesson={lesson} index={index} />
|
||||
<LessonItem key={index} lesson={lesson} index={index} />
|
||||
))}
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop sidebar
|
||||
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">
|
||||
<DesktopSidebarContent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Mobile sidebar implementation - completely restructured for better scrolling
|
||||
if (isMobileView && sidebarVisible) {
|
||||
return (
|
||||
<div className="w-full bg-gray-900 rounded-lg border border-gray-800 shadow-md overflow-hidden mb-4">
|
||||
<div className="bg-gray-800 p-4 border-b border-gray-700">
|
||||
<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
|
||||
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;
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CourseSidebar;
|
||||
export default CourseSidebar;
|
||||
|
@ -12,6 +12,7 @@ const appConfig = {
|
||||
authorPubkeys: [
|
||||
'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741',
|
||||
'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345',
|
||||
'6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4',
|
||||
],
|
||||
customLightningAddresses: [
|
||||
{
|
||||
|
@ -5,17 +5,17 @@ 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 { 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 { Accordion, AccordionTab } from 'primereact/accordion';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { useDecryptContent } from '@/hooks/encryption/useDecryptContent';
|
||||
import dynamic from 'next/dynamic';
|
||||
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
|
||||
import appConfig from '@/config/appConfig';
|
||||
import useWindowWidth from '@/hooks/useWindowWidth';
|
||||
|
||||
const MDDisplay = dynamic(() => import('@uiw/react-markdown-preview'), {
|
||||
ssr: false,
|
||||
@ -176,11 +176,15 @@ const Course = () => {
|
||||
const { ndk, addSigner } = useNDKContext();
|
||||
const { data: session, update } = useSession();
|
||||
const { showToast } = useToast();
|
||||
const [expandedIndex, setExpandedIndex] = useState(null);
|
||||
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 windowWidth = useWindowWidth();
|
||||
const isMobileView = windowWidth <= 968;
|
||||
const [activeTab, setActiveTab] = useState('content'); // Default to content tab on mobile
|
||||
|
||||
const setCompleted = useCallback(lessonId => {
|
||||
setCompletedLessons(prev => [...prev, lessonId]);
|
||||
@ -220,12 +224,20 @@ const Course = () => {
|
||||
if (router.isReady) {
|
||||
const { active } = router.query;
|
||||
if (active !== undefined) {
|
||||
setExpandedIndex(parseInt(active, 10));
|
||||
setActiveIndex(parseInt(active, 10));
|
||||
} 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(() => {
|
||||
if (uniqueLessons.length > 0) {
|
||||
@ -256,15 +268,15 @@ const Course = () => {
|
||||
setNpub(null);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const handleLessonSelect = index => {
|
||||
setActiveIndex(index);
|
||||
router.push(`/course/${router.query.slug}?active=${index}`, undefined, { shallow: true });
|
||||
|
||||
const handleAccordionChange = e => {
|
||||
const newIndex = e.index === expandedIndex ? null : e.index;
|
||||
setExpandedIndex(newIndex);
|
||||
|
||||
if (newIndex !== null) {
|
||||
router.push(`/course/${router.query.slug}?active=${newIndex}`, undefined, { shallow: true });
|
||||
} else {
|
||||
router.push(`/course/${router.query.slug}`, undefined, { shallow: true });
|
||||
// On mobile, switch to content tab after selection
|
||||
if (isMobileView) {
|
||||
setActiveTab('content');
|
||||
setSidebarVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -285,6 +297,15 @@ const Course = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const toggleTab = tab => {
|
||||
setActiveTab(tab);
|
||||
if (tab === 'lessons') {
|
||||
setSidebarVisible(true);
|
||||
} else {
|
||||
setSidebarVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (courseLoading || decryptionLoading) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
@ -339,62 +360,72 @@ const Course = () => {
|
||||
handlePaymentError={handlePaymentError}
|
||||
/>
|
||||
)}
|
||||
<Accordion
|
||||
activeIndex={expandedIndex}
|
||||
onTabChange={handleAccordionChange}
|
||||
className="mt-4 px-4 max-mob:px-0 max-tab:px-0"
|
||||
>
|
||||
{uniqueLessons.length > 0 &&
|
||||
uniqueLessons.map((lesson, index) => (
|
||||
<AccordionTab
|
||||
key={index}
|
||||
pt={{
|
||||
root: { className: 'border-none' },
|
||||
header: { className: 'border-none' },
|
||||
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="mx-4">
|
||||
{/* Mobile tab navigation */}
|
||||
{isMobileView && (
|
||||
<div className="flex w-full border-b border-gray-200 dark:border-gray-700 mb-4">
|
||||
<button
|
||||
className={`flex-1 py-3 font-medium text-center border-b-2 ${
|
||||
activeTab === 'lessons'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
onClick={() => toggleTab('lessons')}
|
||||
>
|
||||
<div className="w-full py-4 rounded-b-lg">
|
||||
{renderLesson(lesson)}
|
||||
{nAddresses[lesson.id] && (
|
||||
<div className="mt-8">
|
||||
{!paidCourse || decryptionPerformed || session?.user?.role?.subscribed ? (
|
||||
<ZapThreadsWrapper
|
||||
anchor={nAddresses[lesson.id]}
|
||||
user={session?.user ? nsec || npub : null}
|
||||
relays={appConfig.defaultRelayUrls.join(',')}
|
||||
disable="zaps"
|
||||
isAuthorized={true}
|
||||
/>
|
||||
) : (
|
||||
<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
|
||||
course creator.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
Course Lessons
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 py-3 font-medium text-center border-b-2 ${
|
||||
activeTab === 'content'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
onClick={() => toggleTab('content')}
|
||||
>
|
||||
Lesson Content
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex relative">
|
||||
{/* Course Sidebar Component */}
|
||||
<CourseSidebar
|
||||
lessons={uniqueLessons}
|
||||
activeIndex={activeIndex}
|
||||
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>
|
||||
</AccordionTab>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="mx-auto my-6">
|
||||
{course?.content && <MDDisplay className="p-4 rounded-lg" source={course.content} />}
|
||||
) : (
|
||||
<div className="text-center bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-8">
|
||||
<p>Select a lesson from the sidebar to begin learning.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user