1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 06:12:53 +02:00
This commit is contained in:
Arik Chakma
2025-07-02 16:21:08 +06:00
parent 40e1b44565
commit bc55f466e7
2 changed files with 112 additions and 32 deletions

View File

@@ -1,56 +1,69 @@
import type { QuizQuestion } from '../../queries/ai-quiz'; import type { QuizQuestion } from '../../queries/ai-quiz';
import { useState } from 'react';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { CheckIcon, XIcon, InfoIcon } from 'lucide-react'; import { CheckIcon, XIcon, InfoIcon } from 'lucide-react';
import { markdownToHtml } from '../../lib/markdown'; import { markdownToHtml } from '../../lib/markdown';
type AIMCQQuestionProps = { type AIMCQQuestionProps = {
question: QuizQuestion; question: QuizQuestion;
onNextQuestion?: () => void; selectedOptions: number[];
setSelectedOptions: (options: number[]) => void;
isSubmitted: boolean;
onSubmit: () => void;
onNext: () => void;
}; };
export function AIMCQQuestion(props: AIMCQQuestionProps) { export function AIMCQQuestion(props: AIMCQQuestionProps) {
const { question, onNextQuestion } = props; const {
question,
selectedOptions,
setSelectedOptions,
isSubmitted,
onSubmit,
onNext,
} = props;
const { title: questionText, options, answerExplanation } = question; const { title: questionText, options, answerExplanation } = question;
const [selectedOptions, setSelectedOptions] = useState<number[]>([]);
const [isSubmitted, setIsSubmitted] = useState(false);
const canSubmitMultipleAnswers = const canSubmitMultipleAnswers =
options.filter((option) => option.isCorrect).length > 1; options.filter((option) => option.isCorrect).length > 1;
const handleSelectOption = (index: number) => { const handleSelectOption = (index: number) => {
if (!canSubmitMultipleAnswers) { if (isSubmitted) {
setSelectedOptions((prev) => (prev.includes(index) ? [] : [index]));
return; return;
} }
setSelectedOptions((prev) => { if (!canSubmitMultipleAnswers) {
if (prev.includes(index)) { const newSelectedOptions = selectedOptions.includes(index)
return prev.filter((id) => id !== index); ? selectedOptions.filter((id) => id !== index)
} : [...selectedOptions, index];
return [...prev, index]; setSelectedOptions(newSelectedOptions);
}); return;
}
const newSelectedOptions = selectedOptions.includes(index)
? selectedOptions.filter((id) => id !== index)
: [...selectedOptions, index];
setSelectedOptions(newSelectedOptions);
}; };
const handleSubmit = () => { const handleSubmit = () => {
if (isSubmitted) { if (isSubmitted) {
onNextQuestion?.(); onNext?.();
setSelectedOptions([]);
setIsSubmitted(false);
return; return;
} }
setIsSubmitted(true); onSubmit();
}; };
const canSubmit = selectedOptions.length > 0; const canSubmit = selectedOptions.length > 0;
const titleHtml = markdownToHtml(questionText, false); const titleHtml = markdownToHtml(questionText, false);
const markdownClassName =
'prose prose-lg prose-p:text-lg prose-p:font-normal prose-p:my-0 prose-pre:my-0 prose-p:prose-code:text-base! prose-p:prose-code:px-2 prose-p:prose-code:py-0.5 prose-p:prose-code:rounded-lg prose-p:prose-code:border prose-p:prose-code:border-black text-left text-black';
return ( return (
<div> <div>
<div <div
className="prose prose-lg prose-p:text-4xl prose-p:font-medium prose-p:my-4 prose-pre:my-0 prose-p:prose-code:text-4xl! prose-p:prose-code:px-3 prose-p:prose-code:rounded-lg prose-p:prose-code:border prose-p:prose-code:border-black text-black" className="prose prose-lg prose-p:text-4xl prose-p:font-medium prose-p:my-4 prose-pre:my-0 prose-p:prose-code:text-3xl! prose-p:prose-code:px-3 prose-p:prose-code:rounded-lg prose-p:prose-code:border prose-p:prose-code:border-black text-black"
dangerouslySetInnerHTML={{ __html: titleHtml }} dangerouslySetInnerHTML={{ __html: titleHtml }}
/> />
@@ -68,11 +81,14 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
const html = markdownToHtml(option.title, false); const html = markdownToHtml(option.title, false);
const isOptionDisabled =
isSubmitted && !isSelected && !isCorrectOption;
return ( return (
<button <button
key={option.id} key={option.id}
className={cn( className={cn(
'flex w-full items-start gap-2 rounded-xl border border-gray-200 p-2 text-lg hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50', 'text-l flex w-full items-start gap-2 rounded-xl border border-gray-200 p-2',
isSelected && !isSubmitted && 'border-gray-400 bg-gray-50', isSelected && !isSubmitted && 'border-gray-400 bg-gray-50',
isSubmitted && isSubmitted &&
isSelectedAndCorrect && isSelectedAndCorrect &&
@@ -83,9 +99,12 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
isSubmitted && isSubmitted &&
isNotSelectedAndCorrect && isNotSelectedAndCorrect &&
'border-green-500 bg-green-50', 'border-green-500 bg-green-50',
!isSelected && !isCorrectOption
? 'hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50'
: '',
)} )}
onClick={() => handleSelectOption(index)} onClick={() => handleSelectOption(index)}
disabled={isSubmitted && !isSelected && !isCorrectOption} disabled={isOptionDisabled}
> >
<div <div
className={cn( className={cn(
@@ -109,7 +128,7 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
{isNotSelectedAndCorrect && <CheckIcon className="size-4" />} {isNotSelectedAndCorrect && <CheckIcon className="size-4" />}
</div> </div>
<div <div
className="prose prose-lg prose-p:text-lg prose-p:font-normal prose-p:my-0 prose-pre:my-0 prose-p:prose-code:text-lg! prose-p:prose-code:px-2 prose-p:prose-code:py-0.5 prose-p:prose-code:rounded-lg prose-p:prose-code:border prose-p:prose-code:border-black mt-0.5 text-left text-black" className={cn(markdownClassName, 'mt-0.5')}
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
</button> </button>
@@ -117,13 +136,18 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
})} })}
</div> </div>
{isSubmitted && ( {isSubmitted && answerExplanation && (
<div className="mt-4 rounded-xl bg-gray-100 p-4"> <div className="mt-4 rounded-xl bg-gray-100 p-4">
<p className="flex items-center gap-2 text-lg text-gray-600"> <p className="flex items-center gap-2 text-lg text-gray-600">
<InfoIcon className="size-4" /> <InfoIcon className="size-4" />
Explanation Explanation
</p> </p>
<p className="mt-1">{answerExplanation}</p> <p
className={cn(markdownClassName, 'mt-0.5')}
dangerouslySetInnerHTML={{
__html: markdownToHtml(answerExplanation, false),
}}
/>
</div> </div>
)} )}

View File

@@ -18,24 +18,74 @@ export function AIQuizContent(props: AIQuizContentProps) {
const [activeQuestionIndex, setActiveQuestionIndex] = useState(0); const [activeQuestionIndex, setActiveQuestionIndex] = useState(0);
const activeQuestion = questions[activeQuestionIndex]; const activeQuestion = questions[activeQuestionIndex];
const [selectedOptions, setSelectedOptions] = useState<
Record<
number,
{
selectedOptions: number[];
isSubmitted: boolean;
}
>
>({});
const hasMoreQuestions = activeQuestionIndex < questions.length - 1; const hasMoreQuestions = activeQuestionIndex < questions.length - 1;
const hasPreviousQuestions = activeQuestionIndex > 0; const hasPreviousQuestions = activeQuestionIndex > 0;
console.log('-'.repeat(20)); const activeQuestionSelectedOptions =
console.log(questions); selectedOptions[activeQuestionIndex]?.selectedOptions ?? [];
console.log('-'.repeat(20)); const activeQuestionIsSubmitted =
selectedOptions[activeQuestionIndex]?.isSubmitted ?? false;
const handleSubmit = (questionIndex: number) => {
setSelectedOptions((prev) => {
const newSelectedOptions = {
...prev,
[questionIndex]: {
selectedOptions: prev[questionIndex].selectedOptions,
isSubmitted: true,
},
};
return newSelectedOptions;
});
};
const handleSelectOptions = (questionIndex: number, options: number[]) => {
setSelectedOptions((prev) => {
const newSelectedOptions = {
...prev,
[questionIndex]: { selectedOptions: options, isSubmitted: false },
};
return newSelectedOptions;
});
};
const progressPercentage = isLoading
? 0
: Math.min(((activeQuestionIndex + 1) / questions.length) * 100, 100);
return ( return (
<div className="mx-auto w-full max-w-lg py-10"> <div className="mx-auto w-full max-w-lg py-10">
<div className="mb-10 flex items-center gap-3"> <div className="mb-10 flex items-center gap-2">
<span className="text-sm text-gray-500">
Question {activeQuestionIndex + 1} of {questions.length}
</span>
<div className="relative h-1.5 mx-2 grow rounded-full bg-gray-200">
<div
className="absolute inset-0 rounded-full bg-black"
style={{
width: `${progressPercentage}%`,
}}
/>
</div>
<NavigationButton <NavigationButton
disabled={!hasPreviousQuestions} disabled={!hasPreviousQuestions}
onClick={() => setActiveQuestionIndex(activeQuestionIndex - 1)} onClick={() => setActiveQuestionIndex(activeQuestionIndex - 1)}
icon={ChevronLeftIcon} icon={ChevronLeftIcon}
/> />
<span className="text-sm text-gray-500">
Question {activeQuestionIndex + 1} of {questions.length}
</span>
<NavigationButton <NavigationButton
disabled={!hasMoreQuestions} disabled={!hasMoreQuestions}
onClick={() => setActiveQuestionIndex(activeQuestionIndex + 1)} onClick={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
@@ -46,7 +96,13 @@ export function AIQuizContent(props: AIQuizContentProps) {
{activeQuestion && activeQuestion.type === 'mcq' && ( {activeQuestion && activeQuestion.type === 'mcq' && (
<AIMCQQuestion <AIMCQQuestion
question={activeQuestion} question={activeQuestion}
onNextQuestion={() => setActiveQuestionIndex(activeQuestionIndex + 1)} selectedOptions={activeQuestionSelectedOptions}
isSubmitted={activeQuestionIsSubmitted}
setSelectedOptions={(options) =>
handleSelectOptions(activeQuestionIndex, options)
}
onSubmit={() => handleSubmit(activeQuestionIndex)}
onNext={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
/> />
)} )}
</div> </div>