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 { 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 = { |
type CertificateViewProps = { |
||||||
|
chapters: ChapterFileType[]; |
||||||
currentCourseId: string; |
currentCourseId: string; |
||||||
}; |
}; |
||||||
|
|
||||||
export function CertificateView(props: CertificateViewProps) { |
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 [rating, setRating] = useState(0); |
||||||
|
const [showRatingForm, setShowRatingForm] = useState(false); |
||||||
|
|
||||||
return ( |
useEffect(() => { |
||||||
<div className="mx-auto flex max-w-md flex-col items-center justify-center"> |
if (!courseProgress) { |
||||||
<div className="flex flex-col items-center"> |
return; |
||||||
<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 |
setIsLoading(false); |
||||||
share it with the world. |
}, [courseProgress]); |
||||||
</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"> |
return ( |
||||||
<Rating |
<> |
||||||
rating={rating} |
{showRatingForm && ( |
||||||
onRatingChange={(rating) => setRating(rating)} |
<RateCourseForm |
||||||
starSize={36} |
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> |
||||||
</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