feat: course landing page

pull/8127/head
Arik Chakma 3 months ago
parent efb8478f56
commit e7272dd001
  1. 91
      src/components/CourseLanding/CourseChapterItem.tsx
  2. 60
      src/components/CourseLanding/CourseFloatingSidebar.tsx
  3. 18
      src/components/CourseLanding/CourseInfoCard.tsx
  4. 242
      src/components/CourseLanding/CourseLanding.tsx

@ -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>
);
}

@ -1,4 +1,5 @@
import {
BookIcon,
CalendarIcon,
CodeXmlIcon,
LetterTextIcon,
@ -9,6 +10,155 @@ import { Rating } from '../Rating/Rating';
import { CourseStatPill } from './CourseStatPill';
import { useRef, useState, useEffect } from 'react';
import { cn } from '../../lib/classname';
import { CourseInfoCard } from './CourseInfoCard';
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
import { CourseChapterItem } from './CourseChapterItem';
import { CourseFloatingSidebar } from './CourseFloatingSidebar';
const DUMMY_COURSE_CONTENT = [
{
title: 'Introduction to SQL',
lessons: [
{
type: 'lesson',
title: 'What is SQL?',
},
{
type: 'lesson',
title: 'Why use SQL?',
},
{
type: 'lesson',
title: 'SQL Syntax',
},
{
type: 'quiz',
title: 'Quiz 1',
},
{
type: 'challenge',
title: 'Challenge 1',
},
],
},
{
title: 'Basic SQL Queries',
lessons: [
{
type: 'lesson',
title: 'SELECT Statement',
},
{
type: 'lesson',
title: 'WHERE Clause',
},
{
type: 'lesson',
title: 'ORDER BY Clause',
},
{
type: 'quiz',
title: 'Quiz 2',
},
{
type: 'challenge',
title: 'Challenge 2',
},
],
},
{
title: 'Advanced SQL Queries',
lessons: [
{
type: 'lesson',
title: 'JOIN Clause',
},
{
type: 'lesson',
title: 'GROUP BY Clause',
},
{
type: 'lesson',
title: 'HAVING Clause',
},
{
type: 'quiz',
title: 'Quiz 3',
},
{
type: 'challenge',
title: 'Challenge 3',
},
],
},
{
title: 'SQL Functions',
lessons: [
{
type: 'lesson',
title: 'COUNT() Function',
},
{
type: 'lesson',
title: 'SUM() Function',
},
{
type: 'lesson',
title: 'AVG() Function',
},
{
type: 'quiz',
title: 'Quiz 4',
},
{
type: 'challenge',
title: 'Challenge 4',
},
],
},
{
title: 'Database Design',
lessons: [
{
type: 'lesson',
title: 'Normalization',
},
{
type: 'lesson',
title: 'Denormalization',
},
{
type: 'lesson',
title: 'Indexes',
},
{
type: 'quiz',
title: 'Quiz 5',
},
{
type: 'challenge',
title: 'Challenge 5',
},
],
},
{
title: 'Optimizing Queries',
lessons: [
{
type: 'lesson',
title: 'Query Optimization',
},
{
type: 'lesson',
title: 'Indexing',
},
{
type: 'lesson',
title: 'Query Caching',
},
],
},
];
export function CourseLanding() {
const containerRef = useRef<HTMLDivElement>(null);
@ -72,11 +222,14 @@ export function CourseLanding() {
</div>
</div>
<div className="container grid grid-cols-5 gap-6 py-8" ref={containerRef}>
<div className="bg-gray-50">
<div
className="container grid grid-cols-5 gap-6 py-8"
ref={containerRef}
>
<div className="col-start-1 col-end-4 space-y-4">
<div className="rounded-md border p-4">
<h2 className="text-xl font-medium">What you'll learn</h2>
<ul className="mt-4 grid grid-cols-2 gap-2">
<CourseInfoCard title="What you'll learn">
<ul className="flex list-inside list-disc flex-col gap-1 text-sm text-gray-700 marker:text-gray-400">
<li>Understand SQL syntax</li>
<li>Write complex queries</li>
<li>Use SQL in real-world scenarios</li>
@ -84,80 +237,45 @@ export function CourseLanding() {
<li>Understand database design</li>
<li>Write complex queries</li>
</ul>
</div>
<div className="rounded-md border p-4">
<h2 className="text-xl font-medium">About this Course</h2>
<div className="prose mt-4">
</CourseInfoCard>
<CourseInfoCard title="About this Course">
<div className="prose-sm mt-4">
<p>
SQL 101 is a beginner-friendly course that will teach you
everything you need to know about SQL. It comes with an
interactive playground where you can practice your queries.
</p>
<p>
The course is divided into multiple sections, each covering a
different aspect of SQL. You'll learn how to write complex
queries, use SQL in real-world scenarios, optimize your queries,
and understand database design.
queries, use SQL in real-world scenarios, optimize your
queries, and understand database design.
</p>
</div>
</CourseInfoCard>
<div className="h-[1000px]"></div>
</div>
</div>
<CourseInfoCard title="Course Content">
{DUMMY_COURSE_CONTENT.map((section, index) => {
const { title, lessons } = section;
const isFirst = index === 0;
const isLast = index === DUMMY_COURSE_CONTENT.length - 1;
<div className="col-start-4 col-end-6">
<div
return (
<CourseChapterItem
key={title}
title={title}
lessons={lessons}
className={cn(
'sticky top-8 -translate-y-1/2 overflow-hidden rounded-lg border bg-white transition-transform',
isSticky && '-translate-y-0',
isFirst ? 'rounded-t-md' : '',
isLast ? 'rounded-b-md' : 'border-b-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 border p-2 px-3">
<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 list-disc 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>
);
})}
</CourseInfoCard>
</div>
<div className="col-start-4 col-end-6">
<CourseFloatingSidebar isSticky={isSticky} />
</div>
</div>
</div>

Loading…
Cancel
Save