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

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

@ -1,15 +1,16 @@
import { useState } from 'react';
import type { ChapterFileType, LessonFileType } from '../../lib/course';
import { Chapter } from './Chapter';
import { StickyNote } from 'lucide-react';
export type CourseSidebarProps = {
courseId: string;
chapterId: string;
lessonId: string;
currentCourseId: string;
currentChapterId?: string;
currentLessonId?: string;
title: string;
chapters: ChapterFileType[];
lesson: LessonFileType;
lesson?: LessonFileType;
completedPercentage: number;
};
@ -19,12 +20,14 @@ export function CourseSidebar(props: CourseSidebarProps) {
title,
chapters,
completedPercentage,
chapterId,
lessonId,
courseId,
currentCourseId,
currentChapterId,
currentLessonId,
} = props;
const [activeChapterId, setActiveChapterId] = useState(chapterId);
const [activeChapterId, setActiveChapterId] = useState(currentChapterId);
const ceritificateUrl = `/learn/${currentCourseId}/certificate`;
return (
<aside className="border-r border-zinc-800">
@ -60,12 +63,20 @@ export function CourseSidebar(props: CourseSidebarProps) {
}}
index={index + 1}
{...chapter}
courseId={courseId}
chapterId={chapterId}
lessonId={lessonId}
currentCourseId={currentCourseId}
currentChapterId={currentChapterId}
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>
</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 = {
courseId: string;
chapterId: string;
lessonId: string;
lessonType: AllowedLessonType;
chapterId?: string;
lessonId?: string;
lessonType?: AllowedLessonType;
challengeStatus?: 'pending' | 'wrong' | 'correct';
quizStatus?: 'pending' | 'wrong' | 'correct';
};

Loading…
Cancel
Save