Major updates on course and lesson layout with tabs

This commit is contained in:
austinkelsay 2025-04-12 13:50:29 -05:00
parent a3e8cda6f4
commit 874d903020
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
4 changed files with 402 additions and 151 deletions

View File

@ -21,6 +21,7 @@ import WelcomeModal from '@/components/onboarding/WelcomeModal';
import { ProgressSpinner } from 'primereact/progressspinner'; import { ProgressSpinner } from 'primereact/progressspinner';
import { Toast } from 'primereact/toast'; import { Toast } from 'primereact/toast';
import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu'; import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu';
import { Divider } from 'primereact/divider';
export default function CourseDetails({ export default function CourseDetails({
processedEvent, processedEvent,
@ -201,54 +202,63 @@ export default function CourseDetails({
<div className="w-full"> <div className="w-full">
<Toast ref={toastRef} /> <Toast ref={toastRef} />
<WelcomeModal /> <WelcomeModal />
<div className="relative w-full h-[400px] mb-8">
<Image <div className="flex flex-col">
alt="course image" {/* Header with course image, title and options */}
src={returnImageProxy(processedEvent.image)} <div className="flex mb-6">
fill {/* Course image */}
className="object-cover rounded-b-lg" <div className="relative w-52 h-32 mr-6 flex-shrink-0 rounded-lg overflow-hidden">
/> <Image
<div className="absolute inset-0 bg-black bg-opacity-20"></div> alt="course image"
</div> src={returnImageProxy(processedEvent.image)}
<div className="w-full mx-auto px-4 py-8 -mt-32 relative z-10 max-mob:px-0 max-tab:px-0"> fill
<i className="object-cover"
className={`pi pi-arrow-left cursor-pointer hover:opacity-75 absolute top-0 left-4`}
onClick={() => router.push('/')}
/>
<div className="mb-8 bg-gray-800/70 rounded-lg p-4 max-mob:rounded-t-none max-tab:rounded-t-none">
{isCompleted && <Tag severity="success" value="Completed" />}
<div className="flex flex-row items-center justify-between w-full">
<h1 className="text-4xl font-bold text-white">{processedEvent.name}</h1>
<ZapDisplay
zapAmount={zapAmount}
event={processedEvent}
zapsLoading={zapsLoading && zapAmount === 0}
/> />
</div> </div>
<div className="flex flex-wrap gap-2 mt-2 mb-4">
{processedEvent.topics && {/* Title and options */}
processedEvent.topics.length > 0 && <div className="flex-1">
processedEvent.topics.map((topic, index) => ( <div className="flex items-start justify-between mb-2">
<Tag className="text-white" key={index} value={topic}></Tag> <div>
))} {isCompleted && (
</div> <Tag severity="success" value="Completed" className="mb-2" />
<div className="text-xl text-gray-200 mb-4 mt-4 max-mob:text-base"> )}
{processedEvent.description && <h1 className="text-2xl font-bold text-white">{processedEvent.name}</h1>
processedEvent.description </div>
.split('\n') <div className="flex items-center space-x-2">
.map((line, index) => <p key={index}>{line}</p>)} <ZapDisplay
</div> zapAmount={zapAmount}
<div className="flex items-center justify-between mt-8"> event={processedEvent}
zapsLoading={zapsLoading && zapAmount === 0}
/>
<MoreOptionsMenu
menuItems={menuItems}
additionalLinks={processedEvent?.additionalLinks || []}
isMobileView={isMobileView}
/>
</div>
</div>
{/* Topics/tags */}
<div className="flex flex-wrap gap-2 mb-3">
{processedEvent.topics &&
processedEvent.topics.length > 0 &&
processedEvent.topics.map((topic, index) => (
<Tag className="text-white" key={index} value={topic}></Tag>
))}
</div>
{/* Author info */}
<div className="flex items-center"> <div className="flex items-center">
<Image <Image
alt="avatar image" alt="avatar image"
src={returnImageProxy(author?.avatar, author?.pubkey)} src={returnImageProxy(author?.avatar, author?.pubkey)}
width={50} width={32}
height={50} height={32}
className="rounded-full mr-4" className="rounded-full mr-2"
/> />
<p className="text-lg text-white"> <p className="text-gray-300">
By{' '} Created by{' '}
<a <a
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
@ -258,15 +268,63 @@ export default function CourseDetails({
</a> </a>
</p> </p>
</div> </div>
<div className="flex justify-end"> </div>
<MoreOptionsMenu </div>
menuItems={menuItems}
additionalLinks={processedEvent?.additionalLinks || []} <Divider className="my-4" />
isMobileView={isMobileView}
/> {/* Course details */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left column: Description */}
<div className="lg:col-span-2">
<h2 className="text-xl font-semibold mb-3 text-white">About This Course</h2>
<div className="text-gray-300 mb-4">
{processedEvent.description &&
processedEvent.description
.split('\n')
.map((line, index) => <p key={index} className="mb-2">{line}</p>)}
</div>
{/* Payment section */}
<div className="mt-4">
{renderPaymentMessage()}
</div>
</div>
{/* Right column: Course details */}
<div className="bg-gray-800 rounded-lg p-4">
<h2 className="text-xl font-semibold mb-3 text-white">Course Information</h2>
<div className="space-y-4">
<div>
<h3 className="text-gray-300 font-medium mb-2">Course Content</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-400">Lessons</p>
<p className="font-semibold text-white">{lessons.length}</p>
</div>
{paidCourse && (
<div>
<p className="text-sm text-gray-400">Price</p>
<p className="font-semibold text-white">{processedEvent.price} sats</p>
</div>
)}
</div>
</div>
{processedEvent.published && (
<div>
<h3 className="text-gray-300 font-medium mb-2">Details</h3>
<div>
<p className="text-sm text-gray-400">Published</p>
<p className="font-semibold text-white">
{new Date(processedEvent.published * 1000).toLocaleDateString()}
</p>
</div>
</div>
)}
</div> </div>
</div> </div>
<div className="w-full mt-4">{renderPaymentMessage()}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Tag } from 'primereact/tag'; import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import { Sidebar } from 'primereact/sidebar';
import Image from 'next/image'; import Image from 'next/image';
import { useImageProxy } from '@/hooks/useImageProxy'; import { useImageProxy } from '@/hooks/useImageProxy';
@ -10,9 +12,26 @@ const CourseSidebar = ({
completedLessons, completedLessons,
isMobileView, isMobileView,
onClose, onClose,
sidebarVisible, sidebarVisible: parentSidebarVisible,
setSidebarVisible,
}) => { }) => {
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const [visible, setVisible] = useState(true);
// Sync with parent state if provided
useEffect(() => {
if (typeof parentSidebarVisible !== 'undefined') {
setVisible(parentSidebarVisible);
}
}, [parentSidebarVisible]);
const handleToggle = () => {
const newState = !visible;
setVisible(newState);
if (setSidebarVisible) {
setSidebarVisible(newState);
}
};
const LessonItem = ({ lesson, index }) => ( const LessonItem = ({ lesson, index }) => (
<li <li
@ -61,51 +80,116 @@ const CourseSidebar = ({
</li> </li>
); );
// Desktop sidebar implementation // Sidebar content component for reuse
const SidebarContent = () => (
<div className="flex flex-col h-full bg-gray-800 text-[#f8f8ff] px-4 py-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>
{visible && (
<Button
icon="pi pi-times"
onClick={handleToggle}
className="p-button-rounded p-button-text text-gray-300 hover:text-white p-button-sm"
tooltip="Close sidebar"
tooltipOptions={{ position: 'left' }}
/>
)}
</div>
<div className="overflow-y-auto flex-1 pr-2">
<ul className="space-y-2">
{lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} index={index} />
))}
</ul>
</div>
</div>
);
// Toggle button (used for both desktop and mobile)
const ToggleButton = () => (
<div className="fixed right-0 top-1/3 z-50 m-0 p-0">
<Button
icon="pi pi-chevron-left"
onClick={handleToggle}
className="shadow-md border-0 rounded-r-none rounded-l-md bg-blue-600 hover:bg-blue-700"
tooltip="Show lessons"
tooltipOptions={{ position: 'left' }}
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
/>
</div>
);
// Desktop implementation with integrated toggle button
if (!isMobileView) { if (!isMobileView) {
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="h-full overflow-y-auto"> {/* Sidebar content */}
<div className="flex flex-col p-4 h-full bg-gray-800 text-[#f8f8ff]"> <div className="relative flex flex-row-reverse">
<div className="flex items-center justify-between border-b border-gray-700 pb-4 mb-4"> <div
<h2 className="font-bold text-white text-lg">Course Lessons</h2> className={`transition-all duration-300 flex ${
</div> visible ? 'w-80' : 'w-0 overflow-hidden'
<div className="overflow-y-auto flex-1"> }`}
<ul className="space-y-2"> >
{lessons.map((lesson, index) => ( <div className="w-80 h-[calc(100vh-400px)] sticky top-8 overflow-hidden rounded-lg border border-gray-800 shadow-md bg-gray-900">
<LessonItem key={index} lesson={lesson} index={index} /> <div className="h-full overflow-y-auto">
))} <SidebarContent />
</ul> </div>
</div> </div>
</div> </div>
</div> </div>
</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 */} {/* Detached toggle button when sidebar is closed */}
<div className="overflow-y-scroll" style={{ maxHeight: '60vh', WebkitOverflowScrolling: 'touch' }}> {!visible && <ToggleButton />}
<div className="p-4 bg-gray-900"> </>
<ul>
{lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} index={index} />
))}
</ul>
</div>
</div>
</div>
); );
} }
return null; // Mobile implementation with PrimeReact's Sidebar
return (
<>
{/* Mobile toggle button - only shown when sidebar is closed */}
{!visible && (
<div className="fixed right-0 top-20 z-40 m-0 p-0">
<Button
icon="pi pi-list"
onClick={handleToggle}
className="shadow-md bg-blue-600 hover:bg-blue-700 border-0 rounded-r-none rounded-l-md"
tooltip="Show lessons"
tooltipOptions={{ position: 'left' }}
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
/>
</div>
)}
{/* Mobile sidebar */}
<Sidebar
visible={visible}
position="right"
onHide={handleToggle}
className="bg-gray-900 p-0 shadow-lg"
style={{ width: '85vw', maxWidth: '350px' }}
showCloseIcon={false}
modal={false}
>
<div className="bg-gray-800 p-5 border-b border-gray-700 flex justify-between items-center">
<h2 className="font-bold text-white text-xl">Course Lessons</h2>
<Button
icon="pi pi-times"
onClick={handleToggle}
className="p-button-rounded p-button-text text-gray-300 hover:text-white"
/>
</div>
<div className="overflow-y-auto h-full p-4 bg-gray-900">
<ul className="space-y-3">
{lessons.map((lesson, index) => (
<LessonItem key={index} lesson={lesson} index={index} />
))}
</ul>
</div>
</Sidebar>
</>
);
}; };
export default CourseSidebar; export default CourseSidebar;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { DataTable } from 'primereact/datatable'; import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column'; import { Column } from 'primereact/column';
import useWindowWidth from '@/hooks/useWindowWidth';
import ProgressListItem from '@/components/content/lists/ProgressListItem'; import ProgressListItem from '@/components/content/lists/ProgressListItem';
import { formatDateTime } from '@/utils/time'; import { formatDateTime } from '@/utils/time';
import { ProgressSpinner } from 'primereact/progressspinner'; import { ProgressSpinner } from 'primereact/progressspinner';

View File

@ -16,6 +16,7 @@ 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'; import useWindowWidth from '@/hooks/useWindowWidth';
import MenuTab from '@/components/menutab/MenuTab';
const MDDisplay = dynamic(() => import('@uiw/react-markdown-preview'), { const MDDisplay = dynamic(() => import('@uiw/react-markdown-preview'), {
ssr: false, ssr: false,
@ -182,10 +183,22 @@ const Course = () => {
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 [sidebarVisible, setSidebarVisible] = useState(false);
const [nAddress, setNAddress] = useState(null);
const windowWidth = useWindowWidth(); const windowWidth = useWindowWidth();
const isMobileView = windowWidth <= 968; const isMobileView = windowWidth <= 968;
const [activeTab, setActiveTab] = useState('content'); // Default to content tab on mobile const [activeTab, setActiveTab] = useState('content'); // Default to content tab on mobile
useEffect(() => {
if (router.isReady) {
const { slug } = router.query;
if (slug.includes('naddr')) {
setNAddress(slug);
} else {
// todo: no naddress?
}
}
}, [router.isReady, router.query.slug]);
const setCompleted = useCallback(lessonId => { const setCompleted = useCallback(lessonId => {
setCompletedLessons(prev => [...prev, lessonId]); setCompletedLessons(prev => [...prev, lessonId]);
}, []); }, []);
@ -297,13 +310,93 @@ const Course = () => {
); );
}; };
const toggleTab = tab => { const toggleTab = (index) => {
setActiveTab(tab); const tabMap = ['overview', 'content', 'qa'];
if (tab === 'lessons') { // If mobile and we have the lessons tab, insert it at index 2
setSidebarVisible(true); if (isMobileView) {
} else { tabMap.splice(2, 0, 'lessons');
setSidebarVisible(false);
} }
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);
}
}
};
// 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 = [
{
label: 'Course Overview',
icon: 'pi pi-home',
},
{
label: 'Lesson Content',
icon: 'pi pi-book',
}
];
// Add lessons tab only on mobile
if (isMobileView) {
items.push({
label: 'Course Lessons',
icon: 'pi pi-list',
});
}
items.push({
label: 'Q&A',
icon: 'pi pi-comments',
});
return items;
};
// Render the QA section (empty for now)
const renderQASection = () => {
return (
<div className="rounded-lg border p-8 mt-4">
<h2 className="text-xl font-bold mb-4">Comments</h2>
<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/"
/>
</div>
);
};
// Render Course Overview section
const renderOverviewSection = () => {
return (
<div className="bg-gray-900 rounded-lg border border-gray-800 shadow-md p-6">
<CourseDetails
processedEvent={course}
paidCourse={paidCourse}
lessons={uniqueLessons}
decryptionPerformed={decryptionPerformed}
handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError}
/>
</div>
);
}; };
if (courseLoading || decryptionLoading) { if (courseLoading || decryptionLoading) {
@ -350,7 +443,7 @@ const Course = () => {
return ( return (
<> <>
{course && paidCourse !== null && ( {/* {course && paidCourse !== null && (
<CourseDetails <CourseDetails
processedEvent={course} processedEvent={course}
paidCourse={paidCourse} paidCourse={paidCourse}
@ -359,71 +452,88 @@ const Course = () => {
handlePaymentSuccess={handlePaymentSuccess} handlePaymentSuccess={handlePaymentSuccess}
handlePaymentError={handlePaymentError} handlePaymentError={handlePaymentError}
/> />
)} )} */}
<div className="mx-4"> <div className="mx-4 mb-12">
{/* Mobile tab navigation */} {/* Tab navigation using MenuTab component */}
{isMobileView && ( <div className="sticky top-0 z-10 pt-2 bg-transparent border-b border-gray-700">
<div className="flex w-full border-b border-gray-200 dark:border-gray-700 mb-4"> <MenuTab
<button items={getTabItems()}
className={`flex-1 py-3 font-medium text-center border-b-2 ${ activeIndex={getActiveTabIndex()}
activeTab === 'lessons' onTabChange={(index) => toggleTab(index)}
? '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')}
>
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}
/> />
</div>
{/* Main content */} <div className="flex items-start mt-4">
{/* Main content area - keep existing implementation */}
<div <div
className={`transition-all duration-200 ${ className={`transition-all duration-300 ${
!isMobileView ? 'ml-8 flex-1' : activeTab === 'content' ? 'w-full' : 'w-full hidden' isMobileView ? 'w-full' : ((!isMobileView && sidebarVisible) ? 'flex-1' : 'w-full')
}`} }`}
> >
{uniqueLessons.length > 0 && uniqueLessons[activeIndex] ? ( {/* Overview tab content */}
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden"> <div className={`${activeTab === 'overview' ? 'block' : 'hidden'}`}>
{renderLesson(uniqueLessons[activeIndex])} {renderOverviewSection()}
</div> </div>
) : (
<div className="text-center bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-8"> {/* Content tab content */}
<p>Select a lesson from the sidebar to begin learning.</p> <div className={`${activeTab === 'content' ? 'block' : 'hidden'}`}>
</div> {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 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 && ( {course?.content && (
<div className="mt-8 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"> <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} /> <MDDisplay className="p-4 rounded-lg" source={course.content} />
</div>
)}
</div>
{/* Lessons tab - only visible on mobile */}
<div className={`${activeTab === 'lessons' && isMobileView ? 'block' : 'hidden'}`}>
<div className="text-center bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-8">
<p>Please use the sidebar to navigate lessons.</p>
</div> </div>
)} </div>
{/* QA tab content */}
<div className={`${activeTab === 'qa' ? 'block' : 'hidden'}`}>
{renderQASection()}
</div>
</div>
{/* Course Sidebar Component - Always visible on desktop, hidden on mobile unless lessons tab is active */}
<div
className={`flex-shrink-0 transition-all duration-300 ${
(!isMobileView && sidebarVisible) ? 'ml-5 w-auto opacity-100' :
(isMobileView && activeTab === 'lessons') ? 'ml-0 w-auto opacity-100' :
'w-0 ml-0 opacity-0 overflow-hidden'
}`}
>
<CourseSidebar
lessons={uniqueLessons}
activeIndex={activeIndex}
onLessonSelect={(index) => {
handleLessonSelect(index);
if (isMobileView) {
toggleTab(getTabItems().findIndex(item => item.label === 'Lesson Content')); // On mobile, switch to content tab when a lesson is selected
}
}}
completedLessons={completedLessons}
isMobileView={isMobileView}
onClose={() => {
setSidebarVisible(false);
setActiveTab('content');
}}
sidebarVisible={sidebarVisible || !isMobileView} // Always visible on desktop
setSidebarVisible={setSidebarVisible}
/>
</div> </div>
</div> </div>
</div> </div>