parent
efb8478f56
commit
e7272dd001
4 changed files with 370 additions and 83 deletions
@ -0,0 +1,91 @@ |
||||
import { BookIcon, ChevronDownIcon, CodeXmlIcon } from 'lucide-react'; |
||||
import { cn } from '../../lib/classname'; |
||||
import type { LessonFrontmatter } from '../../lib/course'; |
||||
import { useMemo, useState } from 'react'; |
||||
|
||||
type CourseChapterItemProps = { |
||||
title: string; |
||||
lessons: { |
||||
type: string; |
||||
title: string; |
||||
}[]; |
||||
className?: string; |
||||
}; |
||||
|
||||
export function CourseChapterItem(props: CourseChapterItemProps) { |
||||
const { title, lessons, className } = props; |
||||
|
||||
const [isOpen, setIsOpen] = useState(false); |
||||
|
||||
const { excercises, textualLessons } = useMemo(() => { |
||||
const excercises: CourseChapterItemProps['lessons'] = []; |
||||
const textualLessons: CourseChapterItemProps['lessons'] = []; |
||||
|
||||
lessons.forEach((lesson) => { |
||||
if (lesson.type === 'quiz' || lesson.type === 'challenge') { |
||||
excercises.push(lesson); |
||||
} else { |
||||
textualLessons.push(lesson); |
||||
} |
||||
}); |
||||
|
||||
return { |
||||
excercises, |
||||
textualLessons, |
||||
}; |
||||
}, [lessons]); |
||||
|
||||
return ( |
||||
<div className={cn('border', className)}> |
||||
<div |
||||
role="button" |
||||
className="flex w-full items-center justify-between gap-1 p-2 pr-3" |
||||
onClick={() => setIsOpen(!isOpen)} |
||||
> |
||||
<span className="text-lg font-medium">{title}</span> |
||||
|
||||
<div className="flex items-center gap-2"> |
||||
{textualLessons.length > 0 && ( |
||||
<span className="text-sm text-gray-500"> |
||||
{textualLessons.length} Lesson |
||||
{textualLessons.length > 1 ? 's' : ''} |
||||
</span> |
||||
)} |
||||
|
||||
{excercises.length > 0 && ( |
||||
<span className="text-sm text-gray-500"> |
||||
{excercises.length} Excerice |
||||
{excercises.length > 1 ? 's' : ''} |
||||
</span> |
||||
)} |
||||
|
||||
<ChevronDownIcon |
||||
className={cn( |
||||
'size-3.5 stroke-[2.5] transition-transform', |
||||
isOpen ? 'rotate-180 transform' : '', |
||||
)} |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
{isOpen && ( |
||||
<div className="border-t"> |
||||
{lessons.map((lesson, index) => { |
||||
return ( |
||||
<div key={index} className="flex items-center gap-2 p-2"> |
||||
<span className="text-gray-500"> |
||||
{lesson.type === 'lesson' ? ( |
||||
<BookIcon className="size-4 stroke-[2.5]" /> |
||||
) : ( |
||||
<CodeXmlIcon className="size-4 stroke-[2.5]" /> |
||||
)} |
||||
</span> |
||||
<span>{lesson.title}</span> |
||||
</div> |
||||
); |
||||
})} |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,60 @@ |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type CourseFloatingSidebarProps = { |
||||
isSticky: boolean; |
||||
}; |
||||
|
||||
export function CourseFloatingSidebar(props: CourseFloatingSidebarProps) { |
||||
const { isSticky } = props; |
||||
|
||||
return ( |
||||
<div |
||||
className={cn( |
||||
'sticky top-8 -translate-y-1/2 overflow-hidden rounded-lg border bg-white shadow-sm transition-transform', |
||||
isSticky && '-translate-y-0', |
||||
)} |
||||
> |
||||
<figure> |
||||
<img |
||||
src="https://images.unsplash.com/photo-1732200584655-3511db5c24e2?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxmZWF0dXJlZC1waG90b3MtZmVlZHw5fHx8ZW58MHx8fHx8" |
||||
alt="SQL 101" |
||||
className="aspect-video w-full object-cover" |
||||
/> |
||||
</figure> |
||||
|
||||
<div className="p-2"> |
||||
<button className="flex w-full items-center justify-between gap-1 rounded-lg bg-gradient-to-r from-purple-500 to-purple-700 p-2 px-3 text-slate-50"> |
||||
<span>Enroll now</span> |
||||
<span>5$ / month</span> |
||||
</button> |
||||
</div> |
||||
|
||||
<div className="border-b p-2 pb-4"> |
||||
<h4 className="text-lg font-medium">Certificate of Completion</h4> |
||||
<p className="text-xs text-gray-500"> |
||||
Certificate will be issued on completion |
||||
</p> |
||||
<figure className="mt-4"> |
||||
<img |
||||
src="https://images.unsplash.com/photo-1732465286852-a0b95393a90d?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxN3x8fGVufDB8fHx8fA%3D%3D" |
||||
alt="SQL 101" |
||||
className="aspect-video w-full rounded-lg object-cover" |
||||
/> |
||||
</figure> |
||||
</div> |
||||
<div className="p-2"> |
||||
<h4 className="text-lg font-medium">What you get</h4> |
||||
<ul |
||||
role="list" |
||||
className="mt-2 flex list-disc flex-col gap-1 pl-4 text-sm text-gray-700 marker:text-gray-400" |
||||
> |
||||
<li>Full access to all the courses</li> |
||||
<li>Personalized access using AI</li> |
||||
<li>Certificate of Completion</li> |
||||
<li>Playground for live-coding</li> |
||||
<li>Challenges / Quizes</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,18 @@ |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type CourseInfoCardProps = { |
||||
title: string; |
||||
children: React.ReactNode; |
||||
className?: string; |
||||
}; |
||||
|
||||
export function CourseInfoCard(props: CourseInfoCardProps) { |
||||
const { title, children, className } = props; |
||||
|
||||
return ( |
||||
<div className={cn('rounded-lg border bg-white p-4 shadow-sm', className)}> |
||||
<h2 className="mb-4 text-xl font-medium">{title}</h2> |
||||
{children} |
||||
</div> |
||||
); |
||||
} |
Loading…
Reference in new issue