Add progress loading skeletons

pull/8127/head
Kamran Ahmed 2 months ago
parent ece427881b
commit d2734b0059
  1. 2
      src/components/Course/CertificateView.tsx
  2. 44
      src/components/Course/Chapter.tsx
  3. 11
      src/components/Course/CircularProgress.tsx
  4. 3
      src/components/Course/CourseLayout.tsx
  5. 23
      src/components/Course/CourseSidebar.tsx
  6. 15
      src/components/Course/CourseSkeletons.tsx

@ -146,7 +146,7 @@ export function CertificateView(props: CertificateViewProps) {
<>
<div className="flex flex-col items-center">
<h1 className="text-4xl font-semibold">Congratulations!</h1>
<p className="mt-3 text-center text-lg text-zinc-200">
<p className="mt-3 text-center text-lg text-gray-600">
You finished the course. Download the completion certificate
below and share it with the world.
</p>

@ -7,6 +7,7 @@ import { CheckIcon } from '../ReactIcons/CheckIcon';
import { getPercentage } from '../../helper/number';
import { useIsMounted } from '../../hooks/use-is-mounted';
import { CircularProgress } from './CircularProgress';
import { ChapterNumberSkeleton, LessonNumberSkeleton } from './CourseSkeletons';
function LeftBorder({ hasCompleted }: { hasCompleted?: boolean }) {
return (
@ -23,6 +24,8 @@ type ChapterProps = ChapterFileType & {
isActive?: boolean;
isCompleted?: boolean;
isLoading?: boolean;
activeCourseId: string;
activeChapterId?: string;
activeLessonId?: string;
@ -38,6 +41,8 @@ export function Chapter(props: ChapterProps) {
isActive = false,
onChapterClick,
isLoading = false,
activeCourseId,
activeChapterId,
activeLessonId,
@ -99,7 +104,8 @@ export function Chapter(props: ChapterProps) {
<CircularProgress
isVisible={!isChapterCompleted}
isActive={isActive}
percentage={Number(completedPercentage) || 5}
isLoading={isLoading}
percentage={Number(completedPercentage) || 0}
>
<div
className={cn(
@ -130,6 +136,7 @@ export function Chapter(props: ChapterProps) {
chapterId={chapterId}
lessons={filteredLessons}
completedLessonSet={completeLessonSet}
isLoading={isLoading}
/>
<div className="relative">
@ -147,6 +154,7 @@ export function Chapter(props: ChapterProps) {
chapterId={chapterId}
lessons={exercises}
completedLessonSet={completeLessonSet}
isLoading={isLoading}
/>
</>
)}
@ -168,6 +176,7 @@ type LessonListProps = {
chapterId: string;
lessons: LessonFileType[];
completedLessonSet: Set<string>;
isLoading?: boolean;
};
function LessonList(props: LessonListProps) {
@ -178,6 +187,7 @@ function LessonList(props: LessonListProps) {
chapterId,
lessons,
completedLessonSet,
isLoading = false,
} = props;
return (
@ -196,6 +206,7 @@ function LessonList(props: LessonListProps) {
chapterId={chapterId}
isActive={isActive}
isCompleted={isCompleted}
isLoading={isLoading}
/>
);
})}
@ -209,6 +220,7 @@ type LessonProps = LessonFileType & {
courseId: string;
counter: number;
chapterId: string;
isLoading?: boolean;
};
export function Lesson(props: LessonProps) {
@ -220,11 +232,10 @@ export function Lesson(props: LessonProps) {
id: lessonId,
counter,
isCompleted,
isLoading = false,
} = props;
const { title } = frontmatter;
const isMounted = useIsMounted();
const { isLoading } = useCourseProgress(courseId);
const href = `/learn/${courseId}/${chapterId}/${lessonId}`;
return (
@ -234,18 +245,21 @@ export function Lesson(props: LessonProps) {
}
href={href}
>
<div
className={cn(
'relative z-10 flex size-5 flex-shrink-0 items-center justify-center rounded-full bg-gray-400/70 text-xs text-white group-hover:bg-gray-400',
{
'bg-black group-hover:bg-black': isActive,
'bg-green-600 group-hover:bg-green-600': !isActive && isCompleted,
},
)}
>
{!isCompleted && counter}
{isCompleted && <Check className={'h-3 w-3 stroke-[3] text-white'} />}
</div>
{!isLoading && (
<div
className={cn(
'relative z-10 flex size-5 flex-shrink-0 items-center justify-center rounded-full bg-gray-400/70 text-xs text-white group-hover:bg-gray-400',
{
'bg-black group-hover:bg-black': isActive,
'bg-green-600 group-hover:bg-green-600': !isActive && isCompleted,
},
)}
>
{!isCompleted && counter}
{isCompleted && <Check className={'h-3 w-3 stroke-[3] text-white'} />}
</div>
)}
{isLoading && <LessonNumberSkeleton />}
<span
className={cn('flex-grow truncate text-left text-gray-600', {
'font-medium text-black': isActive,

@ -1,23 +1,26 @@
import { cn } from '../../lib/classname';
import { ChapterNumberSkeleton } from './CourseSkeletons';
export function CircularProgress({
percentage,
children,
isVisible = true,
isActive = false,
isLoading = false,
}: {
percentage: number;
children: React.ReactNode;
isVisible?: boolean;
isActive?: boolean;
isLoading?: boolean;
}) {
const circumference = 2 * Math.PI * 13;
const strokeDasharray = `${circumference}`;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return (
<div className="relative flex h-[28px] w-[28px] items-center justify-center">
{isVisible && (
<div className="relative flex h-[28px] w-[28px] flex-shrink-0 items-center justify-center">
{isVisible && !isLoading && (
<svg className="absolute h-full w-full -rotate-90">
<circle
cx="14"
@ -37,7 +40,9 @@ export function CircularProgress({
/>
</svg>
)}
{children}
{!isLoading && children}
{isLoading && <ChapterNumberSkeleton />}
</div>
);
}

@ -27,7 +27,7 @@ export function CourseLayout(props: CourseLayoutProps) {
const $currentLesson = useStore(currentLesson);
const [showNextWarning, setShowNextWarning] = useState(false);
const { data: courseProgress } = useCourseProgress(activeCourseId);
const { data: courseProgress, isPending: isCourseProgressPending } = useCourseProgress(activeCourseId);
const completeLesson = useCompleteLessonMutation(activeCourseId);
const completeLessonSet = useMemo(
@ -135,6 +135,7 @@ export function CourseLayout(props: CourseLayoutProps) {
<CourseSidebar
{...sidebarProps}
completedPercentage={Number(courseProgressPercentage)}
isLoading={isCourseProgressPending}
/>
{children}

@ -2,9 +2,11 @@ import { useState } from 'react';
import type { ChapterFileType, LessonFileType } from '../../lib/course';
import { Chapter } from './Chapter';
import { StickyNote, ChevronLeft } from 'lucide-react';
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
import { ProgressPercentageSkeleton } from './CourseSkeletons';
export type CourseSidebarProps = {
isLoading: boolean;
activeCourseId: string;
activeChapterId?: string;
activeLessonId?: string;
@ -24,6 +26,7 @@ export function CourseSidebar(props: CourseSidebarProps) {
activeCourseId,
activeChapterId,
activeLessonId,
isLoading: isProgressLoading,
} = props;
const [openedChapterId, setOpenedChapterId] = useState(activeChapterId);
@ -43,12 +46,17 @@ export function CourseSidebar(props: CourseSidebarProps) {
<div className="border-b">
<div className="px-4 pb-5 pt-7">
<h2 className="mb-1.5 text-2xl font-semibold">{title}</h2>
<div className="text-sm">
<span className="rounded-lg bg-yellow-300 px-1.5 py-0.5 text-black">
{completedPercentage}%
</span>{' '}
Completed
</div>
{!isProgressLoading && (
<div className="text-sm">
<span className="rounded-lg bg-yellow-300 px-1.5 py-0.5 text-black">
{completedPercentage}%
</span>{' '}
Completed
</div>
)}
{isProgressLoading && <ProgressPercentageSkeleton />}
</div>
</div>
@ -61,6 +69,7 @@ export function CourseSidebar(props: CourseSidebarProps) {
<Chapter
key={chapter.id}
isActive={isActive}
isLoading={isProgressLoading}
onChapterClick={() => {
if (isActive) {
setOpenedChapterId('');

@ -0,0 +1,15 @@
export function ProgressPercentageSkeleton() {
return <div className="h-5 w-[120px] animate-pulse rounded bg-gray-200" />;
}
export function ChapterNumberSkeleton() {
return (
<div className="h-[28px] w-[28px] animate-pulse rounded rounded-full bg-gray-200" />
);
}
export function LessonNumberSkeleton() {
return (
<div className="h-[20px] w-[20px] animate-pulse rounded rounded-full bg-gray-300 z-[10]" />
);
}
Loading…
Cancel
Save