Skip to content

Commit ca3797b

Browse files
Create new form for use form
1 parent 17109ce commit ca3797b

File tree

1 file changed

+385
-0
lines changed

1 file changed

+385
-0
lines changed

components/QuizExamFormUF.tsx

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
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

Comments
 (0)