mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-03 07:42:03 +00:00
seperate mobile and desktop components for CourseDetails
This commit is contained in:
parent
b3fae3f557
commit
dae3ed5ad9
@ -1,11 +1,8 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import CoursePaymentButton from '@/components/bitcoinConnect/CoursePaymentButton';
|
||||
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
||||
import GenericButton from '@/components/buttons/GenericButton';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||
@ -20,8 +17,10 @@ import useTrackCourse from '@/hooks/tracking/useTrackCourse';
|
||||
import WelcomeModal from '@/components/onboarding/WelcomeModal';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu';
|
||||
import { Divider } from 'primereact/divider';
|
||||
|
||||
// Import the desktop and mobile components
|
||||
import DesktopCourseDetails from './DesktopCourseDetails';
|
||||
import MobileCourseDetails from './MobileCourseDetails';
|
||||
|
||||
export default function CourseDetails({
|
||||
processedEvent,
|
||||
@ -201,190 +200,33 @@ export default function CourseDetails({
|
||||
);
|
||||
}
|
||||
|
||||
// Shared props for both mobile and desktop components
|
||||
const detailsProps = {
|
||||
processedEvent,
|
||||
paidCourse,
|
||||
lessons,
|
||||
decryptionPerformed,
|
||||
author,
|
||||
zapAmount,
|
||||
zapsLoading,
|
||||
menuItems,
|
||||
returnImageProxy,
|
||||
renderPaymentMessage,
|
||||
isCompleted,
|
||||
showCompletedTag
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full bg-gray-800 p-4 rounded-lg">
|
||||
<Toast ref={toastRef} />
|
||||
<WelcomeModal />
|
||||
|
||||
<div className="flex flex-col">
|
||||
{/* Header with course image, title and options */}
|
||||
{!isPhone && (
|
||||
<div className="flex mb-6">
|
||||
{/* Course image */}
|
||||
<div className="relative w-52 h-32 mr-6 flex-shrink-0 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
alt="course image"
|
||||
src={returnImageProxy(processedEvent.image)}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title and options */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
{isCompleted && showCompletedTag && (
|
||||
<Tag severity="success" value="Completed" className="mb-2" />
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-white">{processedEvent.name}</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
<MoreOptionsMenu
|
||||
menuItems={menuItems}
|
||||
additionalLinks={processedEvent?.additionalLinks || []}
|
||||
isMobileView={isPhone}
|
||||
/>
|
||||
</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">
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(author?.avatar, author?.pubkey)}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full mr-2"
|
||||
/>
|
||||
<p className="text-gray-300">
|
||||
Created by{' '}
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="text-blue-300 hover:underline"
|
||||
>
|
||||
{author?.username || author?.name || author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isPhone ? (
|
||||
<MobileCourseDetails {...detailsProps} />
|
||||
) : (
|
||||
<DesktopCourseDetails {...detailsProps} />
|
||||
)}
|
||||
|
||||
{/* Mobile-specific layout */}
|
||||
{isPhone && (
|
||||
<div className="mb-4">
|
||||
{/* Completed tag is now moved to the parent component */}
|
||||
|
||||
{/* Mobile topics/tags right below image (image is in parent component) */}
|
||||
<div className="flex flex-wrap gap-2 mb-3 mt-2">
|
||||
{processedEvent.topics &&
|
||||
processedEvent.topics.length > 0 &&
|
||||
processedEvent.topics.map((topic, index) => (
|
||||
<Tag className="text-white" key={index} value={topic}></Tag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title and zaps in same row */}
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h1 className="text-xl font-bold text-white mr-3">{processedEvent.name}</h1>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Author info and more options in bottom row */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(author?.avatar, author?.pubkey)}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full mr-2"
|
||||
/>
|
||||
<p className="text-gray-300 text-sm">
|
||||
Created by{' '}
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="text-blue-300 hover:underline"
|
||||
>
|
||||
{author?.username || author?.name || author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<MoreOptionsMenu
|
||||
menuItems={menuItems}
|
||||
additionalLinks={processedEvent?.additionalLinks || []}
|
||||
isMobileView={isPhone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider className="my-4" />
|
||||
|
||||
{/* Course details */}
|
||||
<div className={`grid grid-cols-1 ${isPhone ? 'gap-4' : 'lg:grid-cols-3 gap-6'}`}>
|
||||
{/* Left column: Description */}
|
||||
<div className={`${isPhone ? '' : '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={`${isPhone ? 'text-sm' : ''} 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 h-fit ${isPhone ? 'p-3 pl-0' : 'p-4'}`}>
|
||||
<h2 className={`${isPhone ? 'text-lg' : '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>
|
||||
);
|
||||
|
@ -109,7 +109,7 @@ const CourseSidebar = ({
|
||||
|
||||
// Mobile content tab
|
||||
const MobileLessonsTab = () => (
|
||||
<div className="bg-gray-900 rounded-lg overflow-hidden border border-gray-800 shadow-md mb-6">
|
||||
<div className="bg-gray-800 rounded-lg overflow-hidden border border-gray-800 shadow-md mb-6">
|
||||
<div className="bg-gray-800 p-4 border-b border-gray-700">
|
||||
<h2 className="font-bold text-white text-xl">Course Lessons</h2>
|
||||
</div>
|
||||
@ -138,7 +138,7 @@ const CourseSidebar = ({
|
||||
visible ? 'w-80 opacity-100' : 'w-0 opacity-0 overflow-hidden'
|
||||
}`}
|
||||
>
|
||||
<div className="ml-2 w-80 h-[calc(100vh-400px)] sticky overflow-hidden rounded-lg border border-gray-800 shadow-md bg-gray-900"
|
||||
<div className="ml-2 w-80 h-[calc(100vh-400px)] sticky overflow-hidden rounded-lg border border-gray-800 shadow-md bg-gray-800"
|
||||
style={{ top: `${navbarHeight + 70}px` }}> {/* Adjusted to match new header spacing */}
|
||||
<div className="h-full overflow-y-auto">
|
||||
<SidebarContent />
|
||||
|
151
src/components/content/courses/DesktopCourseDetails.js
Normal file
151
src/components/content/courses/DesktopCourseDetails.js
Normal file
@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { useRouter } from 'next/router';
|
||||
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
||||
import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu';
|
||||
import { Divider } from 'primereact/divider';
|
||||
|
||||
export default function DesktopCourseDetails({
|
||||
processedEvent,
|
||||
paidCourse,
|
||||
lessons,
|
||||
decryptionPerformed,
|
||||
author,
|
||||
zapAmount,
|
||||
zapsLoading,
|
||||
menuItems,
|
||||
returnImageProxy,
|
||||
renderPaymentMessage,
|
||||
isCompleted,
|
||||
showCompletedTag
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header with course image, title and options */}
|
||||
<div className="flex mb-6">
|
||||
{/* Course image */}
|
||||
<div className="relative w-52 h-32 mr-6 flex-shrink-0 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
alt="course image"
|
||||
src={returnImageProxy(processedEvent.image)}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title and options */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
{isCompleted && showCompletedTag && (
|
||||
<Tag severity="success" value="Completed" className="mb-2" />
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-white">{processedEvent.name}</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
<MoreOptionsMenu
|
||||
menuItems={menuItems}
|
||||
additionalLinks={processedEvent?.additionalLinks || []}
|
||||
isMobileView={false}
|
||||
/>
|
||||
</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">
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(author?.avatar, author?.pubkey)}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full mr-2"
|
||||
/>
|
||||
<p className="text-gray-300">
|
||||
Created by{' '}
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="text-blue-300 hover:underline"
|
||||
>
|
||||
{author?.username || author?.name || author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-4" />
|
||||
|
||||
{/* 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 h-fit 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>
|
||||
</>
|
||||
);
|
||||
}
|
134
src/components/content/courses/MobileCourseDetails.js
Normal file
134
src/components/content/courses/MobileCourseDetails.js
Normal file
@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { useRouter } from 'next/router';
|
||||
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
||||
import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu';
|
||||
import { Divider } from 'primereact/divider';
|
||||
|
||||
export default function MobileCourseDetails({
|
||||
processedEvent,
|
||||
paidCourse,
|
||||
lessons,
|
||||
decryptionPerformed,
|
||||
author,
|
||||
zapAmount,
|
||||
zapsLoading,
|
||||
menuItems,
|
||||
returnImageProxy,
|
||||
renderPaymentMessage,
|
||||
isCompleted,
|
||||
showCompletedTag
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile-specific layout */}
|
||||
<div className="mb-4">
|
||||
{/* Topics/tags right below image (image is in parent component) */}
|
||||
<div className="flex flex-wrap gap-2 mb-3 mt-2">
|
||||
{processedEvent.topics &&
|
||||
processedEvent.topics.length > 0 &&
|
||||
processedEvent.topics.map((topic, index) => (
|
||||
<Tag className="text-white" key={index} value={topic}></Tag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title and zaps in same row */}
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h1 className="text-xl font-bold text-white mr-3">{processedEvent.name}</h1>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Author info and more options in bottom row */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
alt="avatar image"
|
||||
src={returnImageProxy(author?.avatar, author?.pubkey)}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full mr-2"
|
||||
/>
|
||||
<p className="text-gray-300 text-sm">
|
||||
Created by{' '}
|
||||
<a
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="text-blue-300 hover:underline"
|
||||
>
|
||||
{author?.username || author?.name || author?.pubkey}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<MoreOptionsMenu
|
||||
menuItems={menuItems}
|
||||
additionalLinks={processedEvent?.additionalLinks || []}
|
||||
isMobileView={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-4" />
|
||||
|
||||
{/* Course details */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* Description */}
|
||||
<div>
|
||||
<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="text-sm mb-2">{line}</p>)}
|
||||
</div>
|
||||
|
||||
{/* Payment section */}
|
||||
<div className="mt-4">
|
||||
{renderPaymentMessage()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Course details */}
|
||||
<div className="bg-gray-800 rounded-lg h-fit p-3 pl-0">
|
||||
<h2 className="text-lg 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -523,29 +523,22 @@ const Course = () => {
|
||||
{/* Content tab content */}
|
||||
<div className={`${activeTab === 'content' ? 'block' : 'hidden'}`}>
|
||||
{uniqueLessons.length > 0 && uniqueLessons[activeIndex] ? (
|
||||
<div className="bg-gray-900 rounded-lg shadow-sm overflow-hidden">
|
||||
<div className="bg-gray-800 rounded-lg shadow-sm overflow-hidden">
|
||||
{renderLesson(uniqueLessons[activeIndex])}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center bg-gray-900 rounded-lg p-8">
|
||||
<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-900 rounded-lg shadow-sm">
|
||||
<div className="mt-8 bg-gray-800 rounded-lg shadow-sm">
|
||||
<MarkdownDisplay content={course.content} className="p-4 rounded-lg" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lessons tab - only visible on mobile */}
|
||||
<div className={`${activeTab === 'lessons' && isMobileView ? 'block' : 'hidden'}`}>
|
||||
<div className="text-center bg-gray-900 rounded-lg p-8">
|
||||
<p>Please use the sidebar to navigate lessons.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QA tab content */}
|
||||
<div className={`${activeTab === 'qa' ? 'block' : 'hidden'}`}>
|
||||
{renderQASection()}
|
||||
|
Loading…
x
Reference in New Issue
Block a user