computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.2 KiB
202 lines
6.2 KiB
import { useState } from 'react'; |
|
import { CourseLayout } from './CourseLayout'; |
|
import { Circle, CircleCheck, CircleX } from 'lucide-react'; |
|
import { cn } from '../../lib/classname'; |
|
import type { |
|
ChapterFileType, |
|
CourseFileType, |
|
LessonFileType, |
|
} from '../../lib/course'; |
|
|
|
type QuizViewProps = { |
|
courseId: string; |
|
chapterId: string; |
|
lessonId: string; |
|
|
|
title: string; |
|
course: CourseFileType & { |
|
chapters: ChapterFileType[]; |
|
}; |
|
lesson: LessonFileType; |
|
}; |
|
|
|
export function QuizView(props: QuizViewProps) { |
|
const { title, course, lesson, courseId, lessonId, chapterId } = props; |
|
const { chapters } = course; |
|
|
|
const { frontmatter } = lesson; |
|
const { questions = [] } = frontmatter; |
|
|
|
const [selectedOptions, setSelectedOptions] = useState< |
|
Record<number, number | undefined> |
|
>({}); |
|
|
|
const [isSubmitted, setIsSubmitted] = useState(false); |
|
|
|
const isAllAnswered = |
|
Object.keys(selectedOptions).length === questions.length; |
|
const correctAnswerCount = questions.filter((question) => { |
|
const selectedOptionId = selectedOptions?.[question.id]; |
|
const correctAnswerId = question.options.find( |
|
(option) => option.isCorrectOption, |
|
)?.id; |
|
|
|
return selectedOptionId === correctAnswerId; |
|
}).length; |
|
|
|
return ( |
|
<CourseLayout |
|
courseId={courseId} |
|
chapterId={chapterId} |
|
lessonId={lesson.id} |
|
lesson={lesson} |
|
title={title} |
|
chapters={chapters} |
|
completedPercentage={0} |
|
> |
|
<div className="relative h-full"> |
|
<div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]"> |
|
<div className="mx-auto max-w-xl p-4 py-10"> |
|
<h3 className="mb-10 text-lg font-semibold"> |
|
SQL Quiz: Intermediate |
|
</h3> |
|
|
|
<div className="flex flex-col gap-3"> |
|
{questions.map((question) => { |
|
return ( |
|
<QuizItem |
|
key={question.id} |
|
id={question.id} |
|
title={question.title} |
|
disabled={isSubmitted} |
|
options={question.options.map((option) => { |
|
const selectedOptionId = selectedOptions?.[question.id]; |
|
|
|
let optionStatus: QuizOptionStatus = 'default'; |
|
if (option.isCorrectOption && isSubmitted) { |
|
optionStatus = 'correct'; |
|
} else if (selectedOptionId === option.id) { |
|
optionStatus = isSubmitted ? 'wrong' : 'selected'; |
|
} |
|
|
|
return { |
|
...option, |
|
status: optionStatus, |
|
}; |
|
})} |
|
onOptionSelectChange={(id, optionId) => { |
|
setSelectedOptions((prev) => ({ |
|
...prev, |
|
[id]: optionId, |
|
})); |
|
}} |
|
selectedOptionId={selectedOptions?.[question.id]} |
|
/> |
|
); |
|
})} |
|
</div> |
|
|
|
<div className="mt-8 flex items-center justify-end"> |
|
<button |
|
className="rounded-xl border border-zinc-700 bg-zinc-800 p-2 px-4 text-sm font-medium text-white focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" |
|
disabled={isSubmitted || !isAllAnswered} |
|
onClick={() => { |
|
setIsSubmitted(true); |
|
}} |
|
> |
|
Submit my Answers |
|
</button> |
|
</div> |
|
|
|
{isSubmitted && ( |
|
<div className="mt-8 flex items-center justify-between gap-2 rounded-xl border border-zinc-800 p-4"> |
|
<span> |
|
You got {correctAnswerCount} out of {questions.length}{' '} |
|
questions right |
|
</span> |
|
|
|
<a className="disabled:cusror-not-allowed rounded-xl border border-zinc-700 bg-zinc-800 p-2 px-4 text-sm font-medium text-white focus:outline-none"> |
|
Move to Next Lesson |
|
</a> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
</CourseLayout> |
|
); |
|
} |
|
|
|
type QuizItemProps = { |
|
id: number; |
|
title: string; |
|
options: QuizOptionProps[]; |
|
|
|
disabled?: boolean; |
|
selectedOptionId?: number; |
|
onOptionSelectChange?: (id: number, optionId: number) => void; |
|
}; |
|
|
|
export function QuizItem(props: QuizItemProps) { |
|
const { id, title, options, onOptionSelectChange, disabled } = props; |
|
|
|
return ( |
|
<div className="rounded-2xl bg-zinc-800 p-4"> |
|
<h3 className="mx-2 text-balance text-lg font-medium">{title}</h3> |
|
|
|
<div className="mt-4 flex flex-col gap-1"> |
|
{options.map((option, index) => { |
|
return ( |
|
<QuizOption |
|
key={index} |
|
id={option.id} |
|
text={option.text} |
|
status={option.status} |
|
onSelect={() => onOptionSelectChange?.(id, option.id)} |
|
disabled={disabled} |
|
/> |
|
); |
|
})} |
|
</div> |
|
</div> |
|
); |
|
} |
|
|
|
type QuizOptionStatus = 'selected' | 'wrong' | 'correct' | 'default'; |
|
|
|
type QuizOptionProps = { |
|
id: number; |
|
text: string; |
|
isCorrectOption?: boolean; |
|
status?: QuizOptionStatus; |
|
|
|
disabled?: boolean; |
|
onSelect?: () => void; |
|
}; |
|
|
|
export function QuizOption(props: QuizOptionProps) { |
|
const { text, status = 'default', onSelect, disabled } = props; |
|
|
|
return ( |
|
<button |
|
onClick={onSelect} |
|
className={cn( |
|
'flex items-start gap-2 rounded-xl p-2 text-sm disabled:cursor-not-allowed', |
|
status === 'selected' && 'ring-1 ring-zinc-500', |
|
status === 'wrong' && 'text-red-500 ring-1 ring-red-500', |
|
status === 'correct' && 'text-green-500 ring-1 ring-green-500', |
|
status === 'default' && 'hover:bg-zinc-700', |
|
)} |
|
disabled={disabled} |
|
> |
|
<span className="mt-0.5"> |
|
{status === 'wrong' && <CircleX className="size-4" />} |
|
{status === 'correct' && <CircleCheck className="size-4" />} |
|
{(status === 'selected' || status === 'default') && ( |
|
<Circle className="size-4" /> |
|
)} |
|
</span> |
|
<p>{text}</p> |
|
</button> |
|
); |
|
}
|
|
|