mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-01 13:22:38 +02:00
wip
This commit is contained in:
@@ -9,24 +9,20 @@ export const markdownClassName =
|
||||
|
||||
type AIMCQQuestionProps = {
|
||||
question: QuizQuestion;
|
||||
selectedOptions: number[];
|
||||
questionState: QuestionState;
|
||||
|
||||
setSelectedOptions: (options: number[]) => void;
|
||||
isSubmitted: boolean;
|
||||
onSubmit: (status: QuestionState['status']) => void;
|
||||
onNext: () => void;
|
||||
};
|
||||
|
||||
export function AIMCQQuestion(props: AIMCQQuestionProps) {
|
||||
const {
|
||||
question,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
isSubmitted,
|
||||
onSubmit,
|
||||
onNext,
|
||||
} = props;
|
||||
const { question, questionState, setSelectedOptions, onSubmit, onNext } =
|
||||
props;
|
||||
const { title: questionText, options, answerExplanation } = question;
|
||||
|
||||
const { isSubmitted, selectedOptions = [], status } = questionState;
|
||||
|
||||
const canSubmitMultipleAnswers =
|
||||
options.filter((option) => option.isCorrect).length > 1;
|
||||
|
||||
|
@@ -17,33 +17,34 @@ type VerifyQuizAnswerResponse = {
|
||||
type AIOpenEndedQuestionProps = {
|
||||
quizSlug: string;
|
||||
question: QuizQuestion;
|
||||
isSubmitted: boolean;
|
||||
questionState: QuestionState;
|
||||
|
||||
onSubmit: (status: QuestionState['status']) => void;
|
||||
onNext: () => void;
|
||||
|
||||
userAnswer: string;
|
||||
setUserAnswer: (answer: string) => void;
|
||||
correctAnswer: string;
|
||||
setCorrectAnswer: (answer: string) => void;
|
||||
|
||||
status: QuestionState['status'];
|
||||
};
|
||||
|
||||
export function AIOpenEndedQuestion(props: AIOpenEndedQuestionProps) {
|
||||
const {
|
||||
quizSlug,
|
||||
question,
|
||||
isSubmitted,
|
||||
questionState,
|
||||
onSubmit,
|
||||
onNext,
|
||||
userAnswer,
|
||||
setUserAnswer,
|
||||
correctAnswer,
|
||||
setCorrectAnswer,
|
||||
status,
|
||||
} = props;
|
||||
const { title: questionText } = question;
|
||||
|
||||
const {
|
||||
isSubmitted,
|
||||
userAnswer = '',
|
||||
correctAnswer = '',
|
||||
status,
|
||||
} = questionState;
|
||||
|
||||
const {
|
||||
mutate: verifyAnswer,
|
||||
isPending: isVerifying,
|
||||
|
@@ -1,12 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import type { QuizQuestion } from '../../queries/ai-quiz';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { AIMCQQuestion } from './AIMCQQuestion';
|
||||
import { AIOpenEndedQuestion } from './AIOpenEndedQuestion';
|
||||
import { QuizTopNavigation } from './QuizTopNavigation';
|
||||
|
||||
export type QuestionState = {
|
||||
isSubmitted: boolean;
|
||||
@@ -39,27 +35,18 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
const [selectedOptions, setSelectedOptions] = useState<
|
||||
Record<number, QuestionState>
|
||||
>({});
|
||||
|
||||
const hasMoreQuestions = activeQuestionIndex < questions.length - 1;
|
||||
const hasPreviousQuestions = activeQuestionIndex > 0;
|
||||
const [isAllQuestionsSubmitted, setIsAllQuestionsSubmitted] = useState(false);
|
||||
|
||||
const activeQuestionState =
|
||||
selectedOptions[activeQuestionIndex] ?? DEFAULT_QUESTION_STATE;
|
||||
|
||||
const activeQuestionSelectedOptions =
|
||||
activeQuestionState.selectedOptions ?? [];
|
||||
const activeQuestionIsSubmitted = activeQuestionState.isSubmitted ?? false;
|
||||
|
||||
const handleSubmit = (
|
||||
questionIndex: number,
|
||||
status: QuestionState['status'],
|
||||
) => {
|
||||
const handleSubmit = (status: QuestionState['status']) => {
|
||||
setSelectedOptions((prev) => {
|
||||
const oldState = activeQuestionState ?? DEFAULT_QUESTION_STATE;
|
||||
|
||||
const newSelectedOptions = {
|
||||
...prev,
|
||||
[questionIndex]: {
|
||||
[activeQuestionIndex]: {
|
||||
...oldState,
|
||||
isSubmitted: true,
|
||||
status,
|
||||
@@ -68,15 +55,17 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
|
||||
return newSelectedOptions;
|
||||
});
|
||||
|
||||
setIsAllQuestionsSubmitted(activeQuestionIndex === questions.length - 1);
|
||||
};
|
||||
|
||||
const handleSetUserAnswer = (questionIndex: number, userAnswer: string) => {
|
||||
const handleSetUserAnswer = (userAnswer: string) => {
|
||||
setSelectedOptions((prev) => {
|
||||
const oldState = activeQuestionState ?? DEFAULT_QUESTION_STATE;
|
||||
|
||||
const newSelectedOptions = {
|
||||
...prev,
|
||||
[questionIndex]: {
|
||||
[activeQuestionIndex]: {
|
||||
...oldState,
|
||||
userAnswer,
|
||||
},
|
||||
@@ -86,16 +75,13 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetCorrectAnswer = (
|
||||
questionIndex: number,
|
||||
correctAnswer: string,
|
||||
) => {
|
||||
const handleSetCorrectAnswer = (correctAnswer: string) => {
|
||||
setSelectedOptions((prev) => {
|
||||
const oldState = activeQuestionState ?? DEFAULT_QUESTION_STATE;
|
||||
|
||||
const newSelectedOptions = {
|
||||
...prev,
|
||||
[questionIndex]: {
|
||||
[activeQuestionIndex]: {
|
||||
...oldState,
|
||||
correctAnswer,
|
||||
},
|
||||
@@ -105,13 +91,13 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectOptions = (questionIndex: number, options: number[]) => {
|
||||
const handleSelectOptions = (options: number[]) => {
|
||||
setSelectedOptions((prev) => {
|
||||
const oldState = activeQuestionState ?? DEFAULT_QUESTION_STATE;
|
||||
|
||||
const newSelectedOptions = {
|
||||
...prev,
|
||||
[questionIndex]: {
|
||||
[activeQuestionIndex]: {
|
||||
...oldState,
|
||||
selectedOptions: options,
|
||||
},
|
||||
@@ -121,88 +107,50 @@ export function AIQuizContent(props: AIQuizContentProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setActiveQuestionIndex(activeQuestionIndex + 1);
|
||||
};
|
||||
|
||||
const totalQuestions = questions?.length ?? 0;
|
||||
const progressPercentage = isLoading
|
||||
? 0
|
||||
: Math.min(((activeQuestionIndex + 1) / questions.length) * 100, 100);
|
||||
: Math.min(((activeQuestionIndex + 1) / totalQuestions) * 100, 100);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-lg py-10">
|
||||
<div className="mb-10 flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">
|
||||
Question {activeQuestionIndex + 1} of {questions.length}
|
||||
</span>
|
||||
<QuizTopNavigation
|
||||
activeQuestionIndex={activeQuestionIndex}
|
||||
totalQuestions={totalQuestions}
|
||||
progressPercentage={progressPercentage}
|
||||
onPrevious={() => setActiveQuestionIndex(activeQuestionIndex - 1)}
|
||||
onNext={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
/>
|
||||
|
||||
<div className="relative mx-2 h-1.5 grow rounded-full bg-gray-200">
|
||||
<div
|
||||
className="absolute inset-0 rounded-full bg-black"
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isAllQuestionsSubmitted && (
|
||||
<>
|
||||
{activeQuestion && activeQuestion.type === 'mcq' && (
|
||||
<AIMCQQuestion
|
||||
question={activeQuestion}
|
||||
questionState={activeQuestionState}
|
||||
setSelectedOptions={handleSelectOptions}
|
||||
onSubmit={handleSubmit}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavigationButton
|
||||
disabled={!hasPreviousQuestions}
|
||||
onClick={() => setActiveQuestionIndex(activeQuestionIndex - 1)}
|
||||
icon={ChevronLeftIcon}
|
||||
/>
|
||||
<NavigationButton
|
||||
disabled={!hasMoreQuestions}
|
||||
onClick={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
icon={ChevronRightIcon}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{activeQuestion && activeQuestion.type === 'mcq' && (
|
||||
<AIMCQQuestion
|
||||
question={activeQuestion}
|
||||
selectedOptions={activeQuestionSelectedOptions}
|
||||
isSubmitted={activeQuestionIsSubmitted}
|
||||
setSelectedOptions={(options) =>
|
||||
handleSelectOptions(activeQuestionIndex, options)
|
||||
}
|
||||
onSubmit={(status) => handleSubmit(activeQuestionIndex, status)}
|
||||
onNext={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeQuestion && activeQuestion.type === 'open-ended' && (
|
||||
<AIOpenEndedQuestion
|
||||
quizSlug={quizSlug ?? ''}
|
||||
question={activeQuestion}
|
||||
isSubmitted={activeQuestionIsSubmitted}
|
||||
onSubmit={(status) => handleSubmit(activeQuestionIndex, status)}
|
||||
onNext={() => setActiveQuestionIndex(activeQuestionIndex + 1)}
|
||||
userAnswer={activeQuestionState.userAnswer ?? ''}
|
||||
setUserAnswer={(userAnswer) =>
|
||||
handleSetUserAnswer(activeQuestionIndex, userAnswer)
|
||||
}
|
||||
correctAnswer={activeQuestionState.correctAnswer ?? ''}
|
||||
setCorrectAnswer={(correctAnswer) =>
|
||||
handleSetCorrectAnswer(activeQuestionIndex, correctAnswer)
|
||||
}
|
||||
status={activeQuestionState.status}
|
||||
/>
|
||||
{activeQuestion && activeQuestion.type === 'open-ended' && (
|
||||
<AIOpenEndedQuestion
|
||||
quizSlug={quizSlug ?? ''}
|
||||
question={activeQuestion}
|
||||
questionState={activeQuestionState}
|
||||
onSubmit={handleSubmit}
|
||||
onNext={handleNext}
|
||||
setUserAnswer={handleSetUserAnswer}
|
||||
setCorrectAnswer={handleSetCorrectAnswer}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type NavigationButtonProps = {
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
function NavigationButton(props: NavigationButtonProps) {
|
||||
const { disabled, onClick, icon: Icon } = props;
|
||||
return (
|
||||
<button
|
||||
className="flex size-7 items-center justify-center rounded-lg border border-gray-200 text-gray-500 hover:text-black disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
74
src/components/AIQuiz/QuizTopNavigation.tsx
Normal file
74
src/components/AIQuiz/QuizTopNavigation.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
type QuizTopNavigationProps = {
|
||||
activeQuestionIndex: number;
|
||||
totalQuestions: number;
|
||||
progressPercentage: number;
|
||||
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
};
|
||||
|
||||
export function QuizTopNavigation(props: QuizTopNavigationProps) {
|
||||
const {
|
||||
activeQuestionIndex,
|
||||
totalQuestions,
|
||||
progressPercentage,
|
||||
onPrevious,
|
||||
onNext,
|
||||
} = props;
|
||||
|
||||
const hasPreviousQuestions = activeQuestionIndex > 0;
|
||||
const hasMoreQuestions = activeQuestionIndex < totalQuestions - 1;
|
||||
|
||||
return (
|
||||
<div className="mb-10 flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">
|
||||
Question {activeQuestionIndex + 1} of {totalQuestions}
|
||||
</span>
|
||||
|
||||
<div className="relative mx-2 h-1.5 grow rounded-full bg-gray-200">
|
||||
<div
|
||||
className="absolute inset-0 rounded-full bg-black"
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NavigationButton
|
||||
disabled={!hasPreviousQuestions}
|
||||
onClick={onPrevious}
|
||||
icon={ChevronLeftIcon}
|
||||
/>
|
||||
<NavigationButton
|
||||
disabled={!hasMoreQuestions}
|
||||
onClick={onNext}
|
||||
icon={ChevronRightIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type NavigationButtonProps = {
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
function NavigationButton(props: NavigationButtonProps) {
|
||||
const { disabled, onClick, icon: Icon } = props;
|
||||
return (
|
||||
<button
|
||||
className="flex size-7 items-center justify-center rounded-lg border border-gray-200 text-gray-500 hover:text-black disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</button>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user