mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-03 06:12:53 +02:00
wip
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user