parent
ac6aab1fe1
commit
fad187b862
7 changed files with 292 additions and 122 deletions
@ -1,42 +1,121 @@ |
||||
import { useState } from 'react'; |
||||
import { useEffect, useMemo, useState } from 'react'; |
||||
import { Rating } from '../Rating/Rating'; |
||||
import { RateCourseForm } from './RateCourseForm'; |
||||
import type { ChapterFileType } from '../../lib/course'; |
||||
import { useCourseProgress } from '../../hooks/use-course'; |
||||
import { Loader2 } from 'lucide-react'; |
||||
|
||||
type CertificateViewProps = { |
||||
chapters: ChapterFileType[]; |
||||
currentCourseId: string; |
||||
}; |
||||
|
||||
export function CertificateView(props: CertificateViewProps) { |
||||
const { currentCourseId } = props; |
||||
const { currentCourseId, chapters } = props; |
||||
|
||||
const [isLoading, setIsLoading] = useState(true); |
||||
|
||||
const { data: courseProgress, status } = useCourseProgress(currentCourseId); |
||||
|
||||
const completeLessonSet = useMemo( |
||||
() => |
||||
new Set( |
||||
(courseProgress?.completed || []).map( |
||||
(l) => `/learn/${currentCourseId}/${l.chapterId}/${l.lessonId}`, |
||||
), |
||||
), |
||||
[courseProgress], |
||||
); |
||||
|
||||
const allLessonLinks = useMemo(() => { |
||||
const lessons: string[] = []; |
||||
for (const chapter of chapters) { |
||||
for (const lesson of chapter.lessons) { |
||||
lessons.push(`/learn/${currentCourseId}/${chapter.id}/${lesson.id}`); |
||||
} |
||||
} |
||||
|
||||
return lessons; |
||||
}, [chapters]); |
||||
|
||||
const isCourseCompleted = useMemo(() => { |
||||
return allLessonLinks.every((lessonLink) => |
||||
completeLessonSet.has(lessonLink), |
||||
); |
||||
}, [allLessonLinks, completeLessonSet]); |
||||
|
||||
const [rating, setRating] = useState(0); |
||||
const [showRatingForm, setShowRatingForm] = useState(false); |
||||
|
||||
return ( |
||||
<div className="mx-auto flex max-w-md flex-col items-center justify-center"> |
||||
<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"> |
||||
You finished the course. Download the completion certificate below and |
||||
share it with the world. |
||||
</p> |
||||
<button> |
||||
<a |
||||
target="_blank" |
||||
rel="noreferrer" |
||||
className="mt-8 block rounded-full bg-zinc-700 px-6 py-2.5 font-medium text-white" |
||||
> |
||||
Download Certificate |
||||
</a> |
||||
</button> |
||||
</div> |
||||
useEffect(() => { |
||||
if (!courseProgress) { |
||||
return; |
||||
} |
||||
|
||||
setIsLoading(false); |
||||
}, [courseProgress]); |
||||
|
||||
<div className="mt-24 flex flex-col items-center gap-3"> |
||||
<Rating |
||||
rating={rating} |
||||
onRatingChange={(rating) => setRating(rating)} |
||||
starSize={36} |
||||
return ( |
||||
<> |
||||
{showRatingForm && ( |
||||
<RateCourseForm |
||||
defaultRating={rating} |
||||
onClose={() => { |
||||
setRating(0); |
||||
setShowRatingForm(false); |
||||
}} |
||||
/> |
||||
<span>Rate your experience</span> |
||||
)} |
||||
|
||||
<div className="mx-auto flex max-w-md flex-col items-center justify-center"> |
||||
{isLoading && ( |
||||
<Loader2 className="size-8 animate-spin stroke-[2.5] text-zinc-200" /> |
||||
)} |
||||
|
||||
{isCourseCompleted && !isLoading && ( |
||||
<> |
||||
<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"> |
||||
You finished the course. Download the completion certificate |
||||
below and share it with the world. |
||||
</p> |
||||
<button> |
||||
<a |
||||
target="_blank" |
||||
rel="noreferrer" |
||||
className="mt-8 block rounded-full bg-zinc-700 px-6 py-2.5 font-medium text-white" |
||||
> |
||||
Download Certificate |
||||
</a> |
||||
</button> |
||||
</div> |
||||
|
||||
<div className="mt-24 flex flex-col items-center gap-3"> |
||||
<Rating |
||||
key={rating} |
||||
rating={rating} |
||||
onRatingChange={(rating) => { |
||||
setRating(rating); |
||||
setShowRatingForm(true); |
||||
}} |
||||
starSize={36} |
||||
/> |
||||
<span>Rate your experience</span> |
||||
</div> |
||||
</> |
||||
)} |
||||
|
||||
{!isCourseCompleted && !isLoading && ( |
||||
<div className="flex flex-col items-center"> |
||||
<h1 className="text-4xl font-semibold">Almost there!</h1> |
||||
<p className="mt-3 text-center text-lg text-zinc-200"> |
||||
Complete the course to download the certificate and rate your |
||||
experience. |
||||
</p> |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
</> |
||||
); |
||||
} |
||||
|
@ -0,0 +1,72 @@ |
||||
import { useState } from 'react'; |
||||
import { Modal } from '../Modal'; |
||||
import { Rating } from '../Rating/Rating'; |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type RateCourseFormProps = { |
||||
defaultRating?: number; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
export function RateCourseForm(props: RateCourseFormProps) { |
||||
const { onClose, defaultRating = 0 } = props; |
||||
|
||||
const [userRating, setUserRating] = useState(defaultRating); |
||||
const [userFeedback, setUserFeedback] = useState(''); |
||||
|
||||
return ( |
||||
<Modal onClose={onClose} bodyClassName="bg-zinc-800 p-5 rounded-lg"> |
||||
<h3 className="font-semibold">Rate this Course</h3> |
||||
<p className="mt-1 text-sm">Share your thoughts with us.</p> |
||||
|
||||
<form |
||||
className="mt-4" |
||||
onSubmit={(e) => { |
||||
e.preventDefault(); |
||||
}} |
||||
> |
||||
<Rating |
||||
rating={userRating} |
||||
onRatingChange={(rating) => { |
||||
setUserRating(rating); |
||||
}} |
||||
starSize={32} |
||||
/> |
||||
<div className="mt-3 flex flex-col gap-1"> |
||||
<label |
||||
htmlFor="rating-feedback" |
||||
className="block text-sm font-medium" |
||||
> |
||||
Feedback{' '} |
||||
<span className="font-normal text-zinc-400">(Optional)</span> |
||||
</label> |
||||
<textarea |
||||
id="rating-feedback" |
||||
className="min-h-24 rounded-md border border-zinc-700 bg-transparent p-2 text-sm outline-none focus:border-zinc-500" |
||||
placeholder="Share your thoughts with the roadmap creator" |
||||
value={userFeedback} |
||||
onChange={(e) => { |
||||
setUserFeedback(e.target.value); |
||||
}} |
||||
/> |
||||
</div> |
||||
|
||||
<div className={cn('mt-4 grid grid-cols-2 gap-1')}> |
||||
<button |
||||
className="h-10 w-full rounded-full border border-zinc-700 p-2.5 text-sm font-medium hover:bg-zinc-700/50 hover:text-white disabled:opacity-60" |
||||
onClick={onClose} |
||||
type="button" |
||||
> |
||||
Cancel |
||||
</button> |
||||
<button |
||||
className="flex h-10 w-full items-center justify-center rounded-full bg-white p-2.5 text-sm font-medium text-black hover:bg-zinc-100 disabled:opacity-60" |
||||
type="submit" |
||||
> |
||||
Submit Rating |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</Modal> |
||||
); |
||||
} |
Loading…
Reference in new issue