|
| 1 | +import { useEffect, useState, type FC } from "react"; |
| 2 | +import Image from "next/image"; |
| 3 | +import { Question } from "./types"; |
| 4 | +import { useForm, useFieldArray, Controller } from "react-hook-form"; |
| 5 | +import { Button } from "./Button"; |
| 6 | +import useResults from "@azure-fundamentals/hooks/useResults"; |
| 7 | + |
| 8 | +type Props = { |
| 9 | + isLoading: boolean; |
| 10 | + handleNextQuestion: (q: number) => void; |
| 11 | + handleSkipQuestion: (q: number) => void; |
| 12 | + handleCountAnswered: () => void; |
| 13 | + currentQuestionIndex: number; |
| 14 | + totalQuestions: number; |
| 15 | + question: string; |
| 16 | + questions: Question[]; |
| 17 | + options: any; |
| 18 | + stopTimer: () => void; |
| 19 | + getResultPoints: (data: number) => void; |
| 20 | + revealExam?: boolean; |
| 21 | + hideExam?: () => void; |
| 22 | + remainingTime?: string; |
| 23 | + link: string; |
| 24 | + images?: { url: string; alt: string }[]; |
| 25 | +}; |
| 26 | + |
| 27 | +const QuizExamFormUF: FC<Props> = ({ |
| 28 | + isLoading, |
| 29 | + handleNextQuestion, |
| 30 | + handleSkipQuestion, |
| 31 | + handleCountAnswered, |
| 32 | + currentQuestionIndex, |
| 33 | + totalQuestions, |
| 34 | + question, |
| 35 | + options, |
| 36 | + stopTimer, |
| 37 | + getResultPoints, |
| 38 | + questions, |
| 39 | + revealExam, |
| 40 | + hideExam, |
| 41 | + remainingTime, |
| 42 | + link, |
| 43 | + images, |
| 44 | +}) => { |
| 45 | + const [showCorrectAnswer, setShowCorrectAnswer] = useState<boolean>(false); |
| 46 | + const [savedAnswers, setSavedAnswers] = useState<any>([]); |
| 47 | + const { points, reCount } = useResults(); |
| 48 | + const [selectedImage, setSelectedImage] = useState<{ |
| 49 | + url: string; |
| 50 | + alt: string; |
| 51 | + } | null>(null); |
| 52 | + const noOfAnswers = options |
| 53 | + ? options?.filter((el: any) => el.isAnswer === true).length |
| 54 | + : 0; |
| 55 | + |
| 56 | + const { control, handleSubmit, setValue, watch } = useForm({ |
| 57 | + defaultValues: { |
| 58 | + options: [{ checked: false, text: "Option 1", isAnswer: false }], |
| 59 | + }, |
| 60 | + }); |
| 61 | + |
| 62 | + const { fields, append, remove } = useFieldArray({ |
| 63 | + control, |
| 64 | + name: "options", |
| 65 | + }); |
| 66 | + |
| 67 | + const onSubmit = (data) => { |
| 68 | + reCount({ questions: questions, answers: savedAnswers }); |
| 69 | + stopTimer(); |
| 70 | + }; |
| 71 | + |
| 72 | + useEffect(() => { |
| 73 | + getResultPoints(points); |
| 74 | + }, [points]); |
| 75 | + |
| 76 | + useEffect(() => { |
| 77 | + if (revealExam) { |
| 78 | + setShowCorrectAnswer(true); |
| 79 | + } |
| 80 | + }, [revealExam]); |
| 81 | + |
| 82 | + useEffect(() => { |
| 83 | + if (remainingTime === "00:00") { |
| 84 | + handleSubmit(onSubmit)(); |
| 85 | + } |
| 86 | + }, [remainingTime]); |
| 87 | + |
| 88 | + useEffect(() => { |
| 89 | + if (savedAnswers.length > 0) { |
| 90 | + let done = true; |
| 91 | + for (let x = 0; x < savedAnswers.length; x++) { |
| 92 | + if (savedAnswers[x] === null) { |
| 93 | + done = false; |
| 94 | + break; |
| 95 | + } |
| 96 | + } |
| 97 | + if (done === true) { |
| 98 | + handleSubmit(onSubmit)(); |
| 99 | + } |
| 100 | + } |
| 101 | + }, [savedAnswers]); |
| 102 | + |
| 103 | + const nextQuestion = (skip: boolean) => { |
| 104 | + saveAnswers(skip); |
| 105 | + let areAllQuestionsAnswered = false; |
| 106 | + let i = currentQuestionIndex + 1; |
| 107 | + while (savedAnswers[i] !== null && i < totalQuestions) { |
| 108 | + i++; |
| 109 | + } |
| 110 | + if (i >= totalQuestions) { |
| 111 | + i = 0; |
| 112 | + } |
| 113 | + while (savedAnswers[i] !== null && i < totalQuestions) { |
| 114 | + i++; |
| 115 | + } |
| 116 | + if (i >= totalQuestions) { |
| 117 | + areAllQuestionsAnswered = true; |
| 118 | + } |
| 119 | + if (skip === true) { |
| 120 | + handleSkipQuestion(i); |
| 121 | + } else { |
| 122 | + if (areAllQuestionsAnswered) { |
| 123 | + handleSubmit(onSubmit)(); |
| 124 | + } else { |
| 125 | + handleCountAnswered(); |
| 126 | + handleNextQuestion(i); |
| 127 | + } |
| 128 | + } |
| 129 | + }; |
| 130 | + |
| 131 | + const isQuestionAnswered = () => { |
| 132 | + for (const answer of fields) { |
| 133 | + if (answer.checked === true) { |
| 134 | + return true; |
| 135 | + } |
| 136 | + } |
| 137 | + return false; |
| 138 | + }; |
| 139 | + |
| 140 | + const isOptionChecked = (optionText: string): boolean | undefined => { |
| 141 | + const savedAnswer = savedAnswers[currentQuestionIndex]; |
| 142 | + return typeof savedAnswer === "string" || !savedAnswer |
| 143 | + ? savedAnswer === optionText |
| 144 | + : savedAnswer.includes(optionText); |
| 145 | + }; |
| 146 | + |
| 147 | + useEffect(() => { |
| 148 | + const savedAnswersInit = Array(totalQuestions).fill(null); |
| 149 | + setSavedAnswers(savedAnswersInit); |
| 150 | + }, [totalQuestions]); |
| 151 | + |
| 152 | + useEffect(() => { |
| 153 | + const opt = options?.map((option) => ({ |
| 154 | + checked: isOptionChecked(option.text), |
| 155 | + text: option.text, |
| 156 | + isAnswer: option.isAnswer, |
| 157 | + })); |
| 158 | + append(opt || []); |
| 159 | + }, [options]); |
| 160 | + |
| 161 | + const saveAnswers = async (skip = false) => { |
| 162 | + if (skip) { |
| 163 | + let saved = [...savedAnswers]; |
| 164 | + saved[currentQuestionIndex] = null; |
| 165 | + setSavedAnswers(saved); |
| 166 | + return; |
| 167 | + } |
| 168 | + |
| 169 | + const options = watch("options"); |
| 170 | + let selectedArr = []; |
| 171 | + let selected = null; |
| 172 | + |
| 173 | + options.forEach((answer) => { |
| 174 | + if (answer.checked && noOfAnswers > 1) { |
| 175 | + selectedArr.push(answer.text); |
| 176 | + } else if (answer.checked && noOfAnswers === 1) { |
| 177 | + selected = answer.text; |
| 178 | + } |
| 179 | + }); |
| 180 | + |
| 181 | + let saved = [...savedAnswers]; |
| 182 | + saved[currentQuestionIndex] = |
| 183 | + noOfAnswers > 1 && selectedArr.length > 0 ? selectedArr : selected; |
| 184 | + setSavedAnswers(saved); |
| 185 | + }; |
| 186 | + |
| 187 | + if (isLoading) return <p>Loading...</p>; |
| 188 | + |
| 189 | + return ( |
| 190 | + <form onSubmit={handleSubmit(onSubmit)}> |
| 191 | + <div className="relative min-h-40"> |
| 192 | + <div className="relative min-h-40 mt-8"> |
| 193 | + <p className="text-white px-12 py-6 select-none"> |
| 194 | + {currentQuestionIndex + 1}. {question} |
| 195 | + </p> |
| 196 | + </div> |
| 197 | + {images && ( |
| 198 | + <ul className="flex flex-row justify-center gap-2 mt-5 mb-8 select-none md:px-12 px-0"> |
| 199 | + {images.map((image) => ( |
| 200 | + <li |
| 201 | + key={image.alt} |
| 202 | + className="w-[40px] h-[40px] rounded-md border border-white overflow-hidden flex flex-row justify-center" |
| 203 | + onClick={() => setSelectedImage(image)} |
| 204 | + > |
| 205 | + <Image |
| 206 | + src={link + image.url} |
| 207 | + alt={image.alt} |
| 208 | + className="max-h-max max-w-max hover:opacity-60" |
| 209 | + unoptimized |
| 210 | + width={200} |
| 211 | + height={200} |
| 212 | + /> |
| 213 | + </li> |
| 214 | + ))} |
| 215 | + </ul> |
| 216 | + )} |
| 217 | + {selectedImage && ( |
| 218 | + <div className="fixed top-0 left-0 z-50 w-full h-full flex justify-center items-center bg-black bg-opacity-50"> |
| 219 | + <img |
| 220 | + src={link + selectedImage.url} |
| 221 | + alt={selectedImage.alt} |
| 222 | + className="max-w-[90%] max-h-[90%]" |
| 223 | + /> |
| 224 | + <button |
| 225 | + onClick={() => setSelectedImage(null)} |
| 226 | + className="absolute top-3 right-5 px-3 py-1 bg-white text-black rounded-md" |
| 227 | + > |
| 228 | + Close |
| 229 | + </button> |
| 230 | + </div> |
| 231 | + )} |
| 232 | + </div> |
| 233 | + <ul className="flex flex-col gap-2 mt-5 mb-16 select-none md:px-12 px-0 h-max min-h-[250px]"> |
| 234 | + {fields.map((option, index) => ( |
| 235 | + <li key={option.id}> |
| 236 | + <Controller |
| 237 | + name={`options.${index}.checked`} |
| 238 | + control={control} |
| 239 | + render={({ field }) => ( |
| 240 | + <input |
| 241 | + {...field} |
| 242 | + type="checkbox" |
| 243 | + id={`options.${index}`} |
| 244 | + disabled={showCorrectAnswer} |
| 245 | + checked={field.value} |
| 246 | + onChange={(e) => { |
| 247 | + if (noOfAnswers === 1) { |
| 248 | + fields.forEach((_, idx) => { |
| 249 | + if (idx !== index) { |
| 250 | + setValue(`options.${idx}.checked`, false); |
| 251 | + } |
| 252 | + }); |
| 253 | + setValue(`options.${index}.checked`, e.target.checked); |
| 254 | + } else { |
| 255 | + setValue(`options.${index}.checked`, e.target.checked); |
| 256 | + } |
| 257 | + }} |
| 258 | + /> |
| 259 | + )} |
| 260 | + /> |
| 261 | + <label |
| 262 | + htmlFor={`options.${index}`} |
| 263 | + className={`m-[1px] flex cursor-pointer items-center rounded-lg border hover:bg-slate-600 p-4 text-xs sm:text-sm font-medium shadow-sm ${ |
| 264 | + showCorrectAnswer && option.isAnswer |
| 265 | + ? option.checked |
| 266 | + ? "border-emerald-500 bg-emerald-500/25 hover:border-emerald-400 hover:bg-emerald-600/50" |
| 267 | + : `border-emerald-500 bg-emerald-500/25 hover:border-emerald-400 hover:bg-emerald-600/50 ${ |
| 268 | + option.checked |
| 269 | + ? "border-emerald-500 bg-emerald-500/50" |
| 270 | + : "" |
| 271 | + }` |
| 272 | + : option.checked |
| 273 | + ? "border-gray-400 bg-gray-500/25 hover:border-gray-300 hover:bg-gray-600" |
| 274 | + : `border-slate-500 bg-gray-600/25 hover:border-gray-400/75 hover:bg-gray-600/75 ${ |
| 275 | + option.checked |
| 276 | + ? "border-gray-400 hover:border-slate-300 bg-gray-600" |
| 277 | + : "" |
| 278 | + }` |
| 279 | + }`} |
| 280 | + > |
| 281 | + <svg |
| 282 | + className={`border ${ |
| 283 | + noOfAnswers > 1 ? "rounded" : "rounded-full" |
| 284 | + } absolute h-5 w-5 p-0.5 ${ |
| 285 | + showCorrectAnswer && option.isAnswer |
| 286 | + ? "text-emerald-500 border-emerald-600" |
| 287 | + : "text-gray-200 border-slate-500" |
| 288 | + }`} |
| 289 | + xmlns="http://www.w3.org/2000/svg" |
| 290 | + viewBox="0 0 16 16" |
| 291 | + fill="currentColor" |
| 292 | + > |
| 293 | + <path |
| 294 | + className={`${option.checked ? "block" : "hidden"}`} |
| 295 | + fillRule="evenodd" |
| 296 | + d="M 2 0 a 2 2 0 0 0 -2 2 v 12 a 2 2 0 0 0 2 2 h 12 a 2 2 0 0 0 2 -2 V 2 a 2 2 0 0 0 -2 -2 H 2 z z" |
| 297 | + clipRule="evenodd" |
| 298 | + /> |
| 299 | + </svg> |
| 300 | + <span className="text-gray-200 pl-7 break-words inline-block w-full"> |
| 301 | + {option?.text} |
| 302 | + </span> |
| 303 | + </label> |
| 304 | + </li> |
| 305 | + ))} |
| 306 | + </ul> |
| 307 | + {!revealExam ? ( |
| 308 | + <div className="flex justify-center flex-col sm:flex-row gap-4"> |
| 309 | + <Button |
| 310 | + type="button" |
| 311 | + intent="primary" |
| 312 | + size="medium" |
| 313 | + onClick={async () => { |
| 314 | + nextQuestion(true); |
| 315 | + }} |
| 316 | + > |
| 317 | + <span>Skip Question</span> |
| 318 | + </Button> |
| 319 | + <Button |
| 320 | + type="button" |
| 321 | + intent="secondary" |
| 322 | + size="medium" |
| 323 | + disabled={!isQuestionAnswered()} |
| 324 | + onClick={async () => { |
| 325 | + nextQuestion(false); |
| 326 | + }} |
| 327 | + > |
| 328 | + <span className={`${!isQuestionAnswered() ? "opacity-50" : ""}`}> |
| 329 | + Next Question |
| 330 | + </span> |
| 331 | + </Button> |
| 332 | + </div> |
| 333 | + ) : ( |
| 334 | + <div className="flex justify-center flex-col sm:flex-row gap-4"> |
| 335 | + <Button |
| 336 | + type="button" |
| 337 | + intent="primary" |
| 338 | + size="medium" |
| 339 | + disabled={currentQuestionIndex === 0} |
| 340 | + onClick={async () => { |
| 341 | + handleNextQuestion(currentQuestionIndex - 1); |
| 342 | + }} |
| 343 | + > |
| 344 | + <span |
| 345 | + className={`${currentQuestionIndex === 0 ? "opacity-50" : ""}`} |
| 346 | + > |
| 347 | + Previous Question |
| 348 | + </span> |
| 349 | + </Button> |
| 350 | + <Button |
| 351 | + type="button" |
| 352 | + intent="primary" |
| 353 | + size="medium" |
| 354 | + disabled={currentQuestionIndex === totalQuestions - 1} |
| 355 | + onClick={async () => { |
| 356 | + handleNextQuestion(currentQuestionIndex + 1); |
| 357 | + }} |
| 358 | + > |
| 359 | + <span |
| 360 | + className={`${ |
| 361 | + currentQuestionIndex === totalQuestions - 1 ? "opacity-50" : "" |
| 362 | + }`} |
| 363 | + > |
| 364 | + Next Question |
| 365 | + </span> |
| 366 | + </Button> |
| 367 | + <Button |
| 368 | + type="button" |
| 369 | + intent="secondary" |
| 370 | + size="medium" |
| 371 | + onClick={() => { |
| 372 | + if (hideExam) { |
| 373 | + hideExam(); |
| 374 | + } |
| 375 | + }} |
| 376 | + > |
| 377 | + <span>Back</span> |
| 378 | + </Button> |
| 379 | + </div> |
| 380 | + )} |
| 381 | + </form> |
| 382 | + ); |
| 383 | +}; |
| 384 | + |
| 385 | +export default QuizExamFormUF; |
0 commit comments