fix: refactor props

feat/course
Arik Chakma 1 month ago
parent ac1da1be10
commit 5529d023dd
  1. 48
      src/components/Course/Chapter.tsx
  2. 142
      src/components/Course/CourseLayout.tsx
  3. 33
      src/components/Course/CourseSidebar.tsx
  4. 65
      src/pages/learn/[courseId]/certificate.astro
  5. 6
      src/stores/course.ts

@ -12,9 +12,9 @@ type ChapterProps = ChapterFileType & {
isActive?: boolean; isActive?: boolean;
isCompleted?: boolean; isCompleted?: boolean;
courseId: string; currentCourseId: string;
chapterId: string; currentChapterId?: string;
lessonId?: string; currentLessonId?: string;
onChapterClick?: () => void; onChapterClick?: () => void;
}; };
@ -26,30 +26,30 @@ export function Chapter(props: ChapterProps) {
isActive = false, isActive = false,
onChapterClick, onChapterClick,
courseId, currentCourseId,
chapterId, currentChapterId,
lessonId, currentLessonId,
} = props; } = props;
const { title } = frontmatter; const { title } = frontmatter;
const { data: courseProgress } = useCourseProgress(courseId); const { data: courseProgress } = useCourseProgress(currentCourseId);
const completeLessonSet = useMemo( const completeLessonSet = useMemo(
() => () =>
new Set( new Set(
(courseProgress?.completed || []) (courseProgress?.completed || [])
.filter((l) => l.chapterId === chapterId) .filter((l) => l.chapterId === currentChapterId)
.map((l) => `${l.chapterId}/${l.lessonId}`), .map((l) => `${l.chapterId}/${l.lessonId}`),
), ),
[courseProgress], [courseProgress],
); );
const isChapterCompleted = lessons.every((lesson) => const isChapterCompleted = lessons.every((lesson) =>
completeLessonSet.has(`${chapterId}/${lesson.id}`), completeLessonSet.has(`${currentChapterId}/${lesson.id}`),
); );
const completedPercentage = useMemo(() => { const completedPercentage = useMemo(() => {
const completedCount = lessons.filter((lesson) => const completedCount = lessons.filter((lesson) =>
completeLessonSet.has(`${chapterId}/${lesson.id}`), completeLessonSet.has(`${currentChapterId}/${lesson.id}`),
).length; ).length;
return getPercentage(completedCount, lessons.length); return getPercentage(completedCount, lessons.length);
@ -103,17 +103,17 @@ export function Chapter(props: ChapterProps) {
<> <>
<div> <div>
{filteredLessons?.map((lesson) => { {filteredLessons?.map((lesson) => {
const isActive = lessonId === lesson.id; const isActive = currentLessonId === lesson.id;
const isCompleted = completeLessonSet.has( const isCompleted = completeLessonSet.has(
`${chapterId}/${lesson.id}`, `${currentChapterId}/${lesson.id}`,
); );
return ( return (
<Lesson <Lesson
key={lesson.id} key={lesson.id}
{...lesson} {...lesson}
courseId={courseId} currentCourseId={currentCourseId}
chapterId={chapterId} currentChapterId={currentChapterId}
isActive={isActive} isActive={isActive}
isCompleted={isCompleted} isCompleted={isCompleted}
/> />
@ -131,17 +131,17 @@ export function Chapter(props: ChapterProps) {
<div> <div>
{exercises?.map((exercise) => { {exercises?.map((exercise) => {
const isActive = lessonId === exercise.id; const isActive = currentLessonId === exercise.id;
const isCompleted = completeLessonSet.has( const isCompleted = completeLessonSet.has(
`${chapterId}/${exercise.id}`, `${currentChapterId}/${exercise.id}`,
); );
return ( return (
<Lesson <Lesson
key={exercise.id} key={exercise.id}
{...exercise} {...exercise}
courseId={courseId} currentCourseId={currentCourseId}
chapterId={chapterId} currentChapterId={currentChapterId}
isActive={isActive} isActive={isActive}
isCompleted={isCompleted} isCompleted={isCompleted}
/> />
@ -161,8 +161,8 @@ export function Chapter(props: ChapterProps) {
} }
type LessonProps = LessonFileType & { type LessonProps = LessonFileType & {
courseId: string; currentCourseId: string;
chapterId: string; currentChapterId?: string;
isActive?: boolean; isActive?: boolean;
isCompleted?: boolean; isCompleted?: boolean;
@ -172,16 +172,16 @@ export function Lesson(props: LessonProps) {
const { const {
frontmatter, frontmatter,
isActive, isActive,
courseId, currentCourseId,
chapterId, currentChapterId,
id: lessonId, id: lessonId,
isCompleted, isCompleted,
} = props; } = props;
const { title } = frontmatter; const { title } = frontmatter;
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const { isLoading } = useCourseProgress(courseId); const { isLoading } = useCourseProgress(currentCourseId);
const href = `/learn/${courseId}/${chapterId}/${lessonId}`; const href = `/learn/${currentCourseId}/${currentChapterId}/${lessonId}`;
return ( return (
<a <a

@ -9,26 +9,33 @@ import { NextLessonAlertModal } from './NextLessonAlertModal';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { currentLesson } from '../../stores/course'; import { currentLesson } from '../../stores/course';
import { getPercentage } from '../../helper/number'; import { getPercentage } from '../../helper/number';
import { cn } from '../../lib/classname';
type CourseLayoutProps = { type CourseLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
} & CourseSidebarProps; } & Omit<CourseSidebarProps, 'completedPercentage'>;
export function CourseLayout(props: CourseLayoutProps) { export function CourseLayout(props: CourseLayoutProps) {
const { children, ...sidebarProps } = props; const { children, ...sidebarProps } = props;
const { chapters, courseId, chapterId, lessonId, lesson } = sidebarProps; const {
chapters,
currentCourseId,
currentChapterId,
currentLessonId,
lesson,
} = sidebarProps;
const $currentLesson = useStore(currentLesson); const $currentLesson = useStore(currentLesson);
const [showNextWarning, setShowNextWarning] = useState(false); const [showNextWarning, setShowNextWarning] = useState(false);
const { data: courseProgress } = useCourseProgress(courseId); const { data: courseProgress } = useCourseProgress(currentCourseId);
const completeLesson = useCompleteLessonMutation(courseId); const completeLesson = useCompleteLessonMutation(currentCourseId);
const completeLessonSet = useMemo( const completeLessonSet = useMemo(
() => () =>
new Set( new Set(
(courseProgress?.completed || []).map( (courseProgress?.completed || []).map(
(l) => `/learn/${courseId}/${l.chapterId}/${l.lessonId}`, (l) => `/learn/${currentCourseId}/${l.chapterId}/${l.lessonId}`,
), ),
), ),
[courseProgress], [courseProgress],
@ -38,7 +45,7 @@ export function CourseLayout(props: CourseLayoutProps) {
const lessons: string[] = []; const lessons: string[] = [];
for (const chapter of chapters) { for (const chapter of chapters) {
for (const lesson of chapter.lessons) { for (const lesson of chapter.lessons) {
lessons.push(`/learn/${courseId}/${chapter.id}/${lesson.id}`); lessons.push(`/learn/${currentCourseId}/${chapter.id}/${lesson.id}`);
} }
} }
@ -53,7 +60,7 @@ export function CourseLayout(props: CourseLayoutProps) {
return getPercentage(completedCount, allLessonLinks.length); return getPercentage(completedCount, allLessonLinks.length);
}, [allLessonLinks, completeLessonSet]); }, [allLessonLinks, completeLessonSet]);
const currentLessonUrl = `/learn/${courseId}/${chapterId}/${lessonId}`; const currentLessonUrl = `/learn/${currentCourseId}/${currentChapterId}/${currentLessonId}`;
const isCurrentLessonCompleted = completeLessonSet.has(currentLessonUrl); const isCurrentLessonCompleted = completeLessonSet.has(currentLessonUrl);
const currentLessonIndex = allLessonLinks.indexOf(currentLessonUrl); const currentLessonIndex = allLessonLinks.indexOf(currentLessonUrl);
@ -68,10 +75,14 @@ export function CourseLayout(props: CourseLayoutProps) {
return; return;
} }
if (!currentChapterId || !currentLessonId) {
return;
}
completeLesson.mutate( completeLesson.mutate(
{ {
chapterId, chapterId: currentChapterId,
lessonId, lessonId: currentLessonId,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@ -91,10 +102,10 @@ export function CourseLayout(props: CourseLayoutProps) {
} }
currentLesson.set({ currentLesson.set({
courseId, courseId: currentCourseId,
chapterId, chapterId: currentChapterId,
lessonId, lessonId: currentLessonId,
lessonType: lesson.frontmatter.type, lessonType: lesson?.frontmatter?.type,
challengeStatus: 'pending', challengeStatus: 'pending',
quizStatus: 'pending', quizStatus: 'pending',
}); });
@ -112,7 +123,14 @@ export function CourseLayout(props: CourseLayoutProps) {
/> />
)} )}
<section className="grid h-screen grid-rows-[1fr_60px] overflow-hidden bg-zinc-900 text-zinc-50"> <section
className={cn(
'grid h-screen grid-rows-[1fr_60px] overflow-hidden bg-zinc-900 text-zinc-50',
currentChapterId && currentLessonId
? 'grid-rows-[1fr_60px]'
: 'grid-rows-1',
)}
>
<div className="grid grid-cols-[240px_1fr] overflow-hidden"> <div className="grid grid-cols-[240px_1fr] overflow-hidden">
<CourseSidebar <CourseSidebar
{...sidebarProps} {...sidebarProps}
@ -122,53 +140,55 @@ export function CourseLayout(props: CourseLayoutProps) {
{children} {children}
</div> </div>
<footer className="flex items-center justify-end border-t border-zinc-800 px-4"> {currentChapterId && currentLessonId && (
<div className="flex items-center gap-2"> <footer className="flex items-center justify-end border-t border-zinc-800 px-4">
<button <div className="flex items-center gap-2">
className="flex items-center gap-1 rounded-lg border border-zinc-800 px-2 py-1.5 text-sm leading-none disabled:opacity-60" <button
onClick={() => { className="flex items-center gap-1 rounded-lg border border-zinc-800 px-2 py-1.5 text-sm leading-none disabled:opacity-60"
window.location.href = prevLessonLink; onClick={() => {
}} window.location.href = prevLessonLink;
disabled={!prevLessonLink || completeLesson.isPending} }}
> disabled={!prevLessonLink || completeLesson.isPending}
<ChevronLeft className="size-4 stroke-[3]" /> >
Prev <ChevronLeft className="size-4 stroke-[3]" />
</button> Prev
</button>
<button
className="flex items-center gap-1 rounded-lg border border-zinc-800 px-2 py-1.5 text-sm leading-none disabled:opacity-60" <button
onClick={() => { className="flex items-center gap-1 rounded-lg border border-zinc-800 px-2 py-1.5 text-sm leading-none disabled:opacity-60"
const isQuizPending = onClick={() => {
($currentLesson?.lessonType === 'lesson-quiz' || const isQuizPending =
$currentLesson?.lessonType === 'quiz') && ($currentLesson?.lessonType === 'lesson-quiz' ||
$currentLesson?.quizStatus === 'pending'; $currentLesson?.lessonType === 'quiz') &&
$currentLesson?.quizStatus === 'pending';
const isChallengePending =
($currentLesson?.lessonType === 'lesson-challenge' || const isChallengePending =
$currentLesson?.lessonType === 'challenge') && ($currentLesson?.lessonType === 'lesson-challenge' ||
$currentLesson?.challengeStatus === 'pending'; $currentLesson?.lessonType === 'challenge') &&
$currentLesson?.challengeStatus === 'pending';
if (
(isQuizPending || isChallengePending) && if (
!isCurrentLessonCompleted (isQuizPending || isChallengePending) &&
) { !isCurrentLessonCompleted
setShowNextWarning(true); ) {
return; setShowNextWarning(true);
} return;
}
handleCompleteLesson();
}} handleCompleteLesson();
disabled={completeLesson.isPending} }}
> disabled={completeLesson.isPending}
Next >
{completeLesson.isPending ? ( Next
<Loader2 className="size-4 animate-spin stroke-[3]" /> {completeLesson.isPending ? (
) : ( <Loader2 className="size-4 animate-spin stroke-[3]" />
<ChevronRight className="size-4 stroke-[3]" /> ) : (
)} <ChevronRight className="size-4 stroke-[3]" />
</button> )}
</div> </button>
</footer> </div>
</footer>
)}
</section> </section>
</> </>
); );

@ -1,15 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import type { ChapterFileType, LessonFileType } from '../../lib/course'; import type { ChapterFileType, LessonFileType } from '../../lib/course';
import { Chapter } from './Chapter'; import { Chapter } from './Chapter';
import { StickyNote } from 'lucide-react';
export type CourseSidebarProps = { export type CourseSidebarProps = {
courseId: string; currentCourseId: string;
chapterId: string; currentChapterId?: string;
lessonId: string; currentLessonId?: string;
title: string; title: string;
chapters: ChapterFileType[]; chapters: ChapterFileType[];
lesson: LessonFileType; lesson?: LessonFileType;
completedPercentage: number; completedPercentage: number;
}; };
@ -19,12 +20,14 @@ export function CourseSidebar(props: CourseSidebarProps) {
title, title,
chapters, chapters,
completedPercentage, completedPercentage,
chapterId, currentCourseId,
lessonId, currentChapterId,
courseId, currentLessonId,
} = props; } = props;
const [activeChapterId, setActiveChapterId] = useState(chapterId); const [activeChapterId, setActiveChapterId] = useState(currentChapterId);
const ceritificateUrl = `/learn/${currentCourseId}/certificate`;
return ( return (
<aside className="border-r border-zinc-800"> <aside className="border-r border-zinc-800">
@ -60,12 +63,20 @@ export function CourseSidebar(props: CourseSidebarProps) {
}} }}
index={index + 1} index={index + 1}
{...chapter} {...chapter}
courseId={courseId} currentCourseId={currentCourseId}
chapterId={chapterId} currentChapterId={currentChapterId}
lessonId={lessonId} currentLessonId={currentLessonId}
/> />
); );
})} })}
<a
className="flex items-center gap-2 p-2 text-sm text-zinc-500 hover:bg-zinc-800 hover:text-white"
href={ceritificateUrl}
>
<StickyNote className="h-4 w-4 stroke-[2.5]" />
Certificate
</a>
</div> </div>
</div> </div>
</aside> </aside>

@ -0,0 +1,65 @@
---
import { CourseLayout } from '../../../components/Course/CourseLayout';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
import {
getAllCourses,
getChaptersByCourseId,
type CourseFileType,
type ChapterFileType,
} from '../../../lib/course';
interface Params extends Record<string, string | undefined> {
courseId: string;
}
interface Props {
course: CourseFileType & { chapters: ChapterFileType[] };
}
export async function getStaticPaths() {
const courses = await getAllCourses();
const coursesWithChapters = await Promise.all(
courses.map(async (course) => {
const chapters = await getChaptersByCourseId(course.id);
return {
...course,
chapters,
};
}),
);
const paths: {
params: Params;
props: Props;
}[] = [];
for (const course of coursesWithChapters) {
const courseId = course.id;
paths.push({
params: {
courseId,
},
props: {
course,
},
});
}
return paths;
}
const { courseId } = Astro.params;
const { course } = Astro.props;
---
<SkeletonLayout title={course.frontmatter.title}>
<CourseLayout
currentCourseId={courseId}
title={course.frontmatter.title}
chapters={course.chapters}
client:load
>
Hello
</CourseLayout>
</SkeletonLayout>

@ -3,9 +3,9 @@ import type { AllowedLessonType } from '../lib/course';
export type CurrentLessonType = { export type CurrentLessonType = {
courseId: string; courseId: string;
chapterId: string; chapterId?: string;
lessonId: string; lessonId?: string;
lessonType: AllowedLessonType; lessonType?: AllowedLessonType;
challengeStatus?: 'pending' | 'wrong' | 'correct'; challengeStatus?: 'pending' | 'wrong' | 'correct';
quizStatus?: 'pending' | 'wrong' | 'correct'; quizStatus?: 'pending' | 'wrong' | 'correct';
}; };

Loading…
Cancel
Save