Add progress loading skeletons

pull/8127/head
Kamran Ahmed 2 months ago
parent ece427881b
commit d2734b0059
  1. 2
      src/components/Course/CertificateView.tsx
  2. 20
      src/components/Course/Chapter.tsx
  3. 11
      src/components/Course/CircularProgress.tsx
  4. 3
      src/components/Course/CourseLayout.tsx
  5. 11
      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"> <div className="flex flex-col items-center">
<h1 className="text-4xl font-semibold">Congratulations!</h1> <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 You finished the course. Download the completion certificate
below and share it with the world. below and share it with the world.
</p> </p>

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

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

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

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