mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-02 13:52:46 +02:00
wip
This commit is contained in:
@@ -1,56 +1,69 @@
|
||||
import type { QuizQuestion } from '../../queries/ai-quiz';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { CheckIcon, XIcon, InfoIcon } from 'lucide-react';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
|
||||
type AIMCQQuestionProps = {
|
||||
question: QuizQuestion;
|
||||
onNextQuestion?: () => void;
|
||||
selectedOptions: number[];
|
||||
setSelectedOptions: (options: number[]) => void;
|
||||
isSubmitted: boolean;
|
||||
onSubmit: () => void;
|
||||
onNext: () => void;
|
||||
};
|
||||
|
||||
export function AIMCQQuestion(props: AIMCQQuestionProps) {
|
||||
const { question, onNextQuestion } = props;
|
||||
const {
|
||||
question,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
isSubmitted,
|
||||
onSubmit,
|
||||
onNext,
|
||||
} = props;
|
||||
const { title: questionText, options, answerExplanation } = question;
|
||||
|
||||
const [selectedOptions, setSelectedOptions] = useState<number[]>([]);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const canSubmitMultipleAnswers =
|
||||
options.filter((option) => option.isCorrect).length > 1;
|
||||
|
||||
const handleSelectOption = (index: number) => {
|
||||
if (!canSubmitMultipleAnswers) {
|
||||
setSelectedOptions((prev) => (prev.includes(index) ? [] : [index]));
|
||||
if (isSubmitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedOptions((prev) => {
|
||||
if (prev.includes(index)) {
|
||||
return prev.filter((id) => id !== index);
|
||||
}
|
||||
return [...prev, index];
|
||||
});
|
||||
if (!canSubmitMultipleAnswers) {
|
||||
const newSelectedOptions = selectedOptions.includes(index)
|
||||
? selectedOptions.filter((id) => id !== index)
|
||||
: [...selectedOptions, index];
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelectedOptions = selectedOptions.includes(index)
|
||||
? selectedOptions.filter((id) => id !== index)
|
||||
: [...selectedOptions, index];
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (isSubmitted) {
|
||||
onNextQuestion?.();
|
||||
setSelectedOptions([]);
|
||||
setIsSubmitted(false);
|
||||
onNext?.();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitted(true);
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
const canSubmit = selectedOptions.length > 0;
|
||||
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 (
|
||||
<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 }}
|
||||
/>
|
||||
|
||||
@@ -68,11 +81,14 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
|
||||
|
||||
const html = markdownToHtml(option.title, false);
|
||||
|
||||
const isOptionDisabled =
|
||||
isSubmitted && !isSelected && !isCorrectOption;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
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',
|
||||
isSubmitted &&
|
||||
isSelectedAndCorrect &&
|
||||
@@ -83,9 +99,12 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
|
||||
isSubmitted &&
|
||||
isNotSelectedAndCorrect &&
|
||||
'border-green-500 bg-green-50',
|
||||
!isSelected && !isCorrectOption
|
||||
? 'hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
: '',
|
||||
)}
|
||||
onClick={() => handleSelectOption(index)}
|
||||
disabled={isSubmitted && !isSelected && !isCorrectOption}
|
||||
disabled={isOptionDisabled}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -109,7 +128,7 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
|
||||
{isNotSelectedAndCorrect && <CheckIcon className="size-4" />}
|
||||
</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 }}
|
||||
/>
|
||||
</button>
|
||||
@@ -117,13 +136,18 @@ export function AIMCQQuestion(props: AIMCQQuestionProps) {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isSubmitted && (
|
||||
{isSubmitted && answerExplanation && (
|
||||
<div className="mt-4 rounded-xl bg-gray-100 p-4">
|
||||
<p className="flex items-center gap-2 text-lg text-gray-600">
|
||||
<InfoIcon className="size-4" />
|
||||
Explanation
|
||||
</p>
|
||||
<p className="mt-1">{answerExplanation}</p>
|
||||
<p
|
||||
className={cn(markdownClassName, 'mt-0.5')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToHtml(answerExplanation, false),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@@ -18,24 +18,74 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
const [activeQuestionIndex, setActiveQuestionIndex] = useState(0);
|
||||
const activeQuestion = questions[activeQuestionIndex];
|
||||
|
||||
const [selectedOptions, setSelectedOptions] = useState<
|
||||
Record<
|
||||
number,
|
||||
{
|
||||
selectedOptions: number[];
|
||||
isSubmitted: boolean;
|
||||
}
|
||||
>
|
||||
>({});
|
||||
|
||||
const hasMoreQuestions = activeQuestionIndex < questions.length - 1;
|
||||
const hasPreviousQuestions = activeQuestionIndex > 0;
|
||||
|
||||
console.log('-'.repeat(20));
|
||||
console.log(questions);
|
||||
console.log('-'.repeat(20));
|
||||
const activeQuestionSelectedOptions =
|
||||
selectedOptions[activeQuestionIndex]?.selectedOptions ?? [];
|
||||
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 (
|
||||
<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
|
||||
disabled={!hasPreviousQuestions}
|
||||
onClick={() => setActiveQuestionIndex(activeQuestionIndex - 1)}
|
||||
icon={ChevronLeftIcon}
|
||||
/>
|
||||
<span className="text-sm text-gray-500">
|
||||
Question {activeQuestionIndex + 1} of {questions.length}
|
||||
</span>
|
||||
<NavigationButton
|
||||
disabled={!hasMoreQuestions}
|
||||
onClick={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
@@ -46,7 +96,13 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
{activeQuestion && activeQuestion.type === 'mcq' && (
|
||||
<AIMCQQuestion
|
||||
question={activeQuestion}
|
||||
onNextQuestion={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
selectedOptions={activeQuestionSelectedOptions}
|
||||
isSubmitted={activeQuestionIsSubmitted}
|
||||
setSelectedOptions={(options) =>
|
||||
handleSelectOptions(activeQuestionIndex, options)
|
||||
}
|
||||
onSubmit={() => handleSubmit(activeQuestionIndex)}
|
||||
onNext={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user