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