wizr-quiz 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/index.js +0 -1
  2. package/dist/index.mjs +0 -1
  3. package/package.json +12 -4
  4. package/rollup.config-1745339193645.cjs +0 -50
  5. package/rollup.config.js +0 -45
  6. package/src/assets/Timer.svg +0 -3
  7. package/src/assets/close.svg +0 -4
  8. package/src/assets/downArrow.svg +0 -11
  9. package/src/assets/fonts/patron/patron-black.woff2 +0 -0
  10. package/src/assets/fonts/patron/patron-blackitalic.woff2 +0 -0
  11. package/src/assets/fonts/patron/patron-bold.woff2 +0 -0
  12. package/src/assets/fonts/patron/patron-bolditalic.woff2 +0 -0
  13. package/src/assets/fonts/patron/patron-italic.woff2 +0 -0
  14. package/src/assets/fonts/patron/patron-light.woff2 +0 -0
  15. package/src/assets/fonts/patron/patron-lightitalic.woff2 +0 -0
  16. package/src/assets/fonts/patron/patron-medium.woff2 +0 -0
  17. package/src/assets/fonts/patron/patron-mediumitalic.woff2 +0 -0
  18. package/src/assets/fonts/patron/patron-regular.woff2 +0 -0
  19. package/src/assets/fonts/patron/patron-thin.woff2 +0 -0
  20. package/src/assets/fonts/patron/patron-thinitalic.woff2 +0 -0
  21. package/src/assets/fonts/quintus/Quintus_B_trial.ttf +0 -0
  22. package/src/assets/fonts/quintus/Quintus_R_trial.ttf +0 -0
  23. package/src/assets/react.svg +0 -1
  24. package/src/components/Options.tsx +0 -27
  25. package/src/components/Question.tsx +0 -18
  26. package/src/components/Quiz.tsx +0 -297
  27. package/src/components/Timer.tsx +0 -53
  28. package/src/components/loader.tsx +0 -12
  29. package/src/components/quizEndScreen.tsx +0 -20
  30. package/src/components/tabSwitchModal.tsx +0 -30
  31. package/src/hooks/useDisableCopyPaste.ts +0 -25
  32. package/src/hooks/useTabSwitch.tsx +0 -29
  33. package/src/index.ts +0 -3
  34. package/src/styles/fonts.css +0 -44
  35. package/src/styles/loader.css +0 -27
  36. package/src/styles/options.css +0 -58
  37. package/src/styles/question.css +0 -40
  38. package/src/styles/quiz.css +0 -267
  39. package/src/styles/quizEndScreen.css +0 -33
  40. package/src/styles/tabSwitchModal.css +0 -78
  41. package/src/styles/timer.css +0 -5
  42. package/src/svg.d.ts +0 -6
  43. package/src/types/index.ts +0 -60
  44. package/tsconfig.json +0 -20
  45. package/wizr-quiz-1.0.0.tgz +0 -0
  46. package/wizr-sdk-1.0.0.tgz +0 -0
@@ -1,297 +0,0 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
- import Question from "./Question";
3
- import Options from "./Options";
4
- import Timer from "./Timer";
5
- import useTabSwitch from "../hooks/useTabSwitch";
6
- import TabSwitchModal from "./tabSwitchModal";
7
- import QuizEndScreen from "./quizEndScreen";
8
- import useDisableCopyPaste from "../hooks/useDisableCopyPaste";
9
- import { IQuizProps, QuizData } from "../types";
10
- import DownIconUrl from "../assets/downArrow.svg";
11
- import Loader from "./loader";
12
- import "../styles/quiz.css";
13
- import "../styles/fonts.css";
14
-
15
- const Quiz = ({
16
- quizId,
17
- userId,
18
- tabSwitchLimit,
19
- onQuizComplete,
20
- baseURL,
21
- token,
22
- }: IQuizProps) => {
23
- const [isQuizEnd, setIsQuizEnd] = useState(false);
24
- const [currentStep, setCurrentStep] = useState(0);
25
- const [selectedOption, setSelectedOption] = useState<string | null>(null);
26
- const [gameData, setGameData] = useState<QuizData | null>(null);
27
- const [isModalOpen, setIsModalOpen] = useState(false);
28
- const [isEndScreenOpen, setIsEndScreenOpen] = useState(false);
29
- const [loading, setLoading] = useState(false);
30
- const { isLimitReached, switchCount } = useTabSwitch(tabSwitchLimit);
31
-
32
- const timeoutIdsRef = useRef<number[]>([]);
33
-
34
- useDisableCopyPaste();
35
-
36
- useEffect(() => {
37
- (async () => {
38
- try {
39
- const res = await fetch(
40
- `${baseURL}/user-assessment/${quizId}/${userId}/get-session-data`,
41
- {
42
- headers: {
43
- "Authorization": `Bearer ${token}`
44
- }
45
- }
46
- );
47
- const data = await res.json();
48
- setGameData(data);
49
- } catch (error) {
50
- // console.error("Error fetching quiz data:", error);
51
- }
52
- })();
53
- }, [quizId, userId]);
54
-
55
- const quizContainerRef = useRef<HTMLDivElement | null>(null);
56
- const [isScrollable, setIsScrollable] = useState(false);
57
- const [hasManuallyScrolledToBottom, setHasManuallyScrolledToBottom] =
58
- useState(false);
59
-
60
- useEffect(() => {
61
- if (!gameData) return;
62
- const el = quizContainerRef.current;
63
- if (!el) return;
64
-
65
- const checkScroll = () => {
66
- const hasOverflow = el.scrollHeight > el.clientHeight;
67
- const isAtBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 15;
68
-
69
- if (hasOverflow) {
70
- if (isAtBottom) {
71
- setHasManuallyScrolledToBottom(true);
72
- }
73
-
74
- if (!isAtBottom && !hasManuallyScrolledToBottom) {
75
- setIsScrollable(true);
76
- } else {
77
- setIsScrollable(false);
78
- }
79
- } else {
80
- setIsScrollable(false);
81
- setHasManuallyScrolledToBottom(false);
82
- }
83
- };
84
-
85
- // ✅ Delay scroll check until after DOM paint
86
- const raf = requestAnimationFrame(() => {
87
- setTimeout(checkScroll, 100); // small delay to allow full DOM layout
88
- });
89
-
90
- el.addEventListener("scroll", checkScroll);
91
- window.addEventListener("resize", checkScroll);
92
-
93
- return () => {
94
- cancelAnimationFrame(raf);
95
- el.removeEventListener("scroll", checkScroll);
96
- window.removeEventListener("resize", checkScroll);
97
- };
98
- }, [gameData, currentStep, hasManuallyScrolledToBottom]);
99
-
100
- const scrollToBottom = () => {
101
- const el = quizContainerRef.current;
102
- if (el) {
103
- el.scrollTo({
104
- top: el.scrollHeight + 10,
105
- behavior: "smooth",
106
- });
107
- setHasManuallyScrolledToBottom(true); // Mark as manually scrolled
108
- setIsScrollable(false); // hide button after click
109
- }
110
- };
111
-
112
- useEffect(() => {
113
- if (switchCount > 0 && switchCount < 3) {
114
- setIsModalOpen(true);
115
- }
116
- }, [switchCount]);
117
-
118
- useEffect(() => {
119
- if (isLimitReached) {
120
- setIsEndScreenOpen(true);
121
- }
122
- }, [isLimitReached]);
123
-
124
- useEffect(() => {
125
- return () => {
126
- timeoutIdsRef.current.forEach((id) => clearTimeout(id));
127
- timeoutIdsRef.current = [];
128
- };
129
- }, []);
130
-
131
- if (!gameData) {
132
- return <Loader />;
133
- }
134
-
135
- function resetModalState() {
136
- setIsModalOpen(false);
137
- setIsEndScreenOpen(false);
138
- }
139
-
140
- function handleQuizClose() {
141
- setIsQuizEnd(true);
142
- }
143
-
144
- const questions = gameData.questions;
145
-
146
- const submitAnswer = async (
147
- quizId: string,
148
- questionId: string,
149
- userId: string,
150
- selectedOption: string
151
- ) => {
152
- try {
153
- await fetch(
154
- `${baseURL}/user-assessment/${quizId}/${userId}/${questionId}/submit-answer`,
155
- {
156
- method: "POST",
157
- headers: {
158
- "Content-Type": "application/json",
159
- "Authorization": `Bearer ${token}`
160
- },
161
- body: JSON.stringify({
162
- optionIds: [selectedOption],
163
- }),
164
- }
165
- );
166
- } catch (error) {
167
- // console.error("Error submitting answer:", error);
168
- }
169
- };
170
-
171
- const markQuizComplete = async () => {
172
- try {
173
- const response = await fetch(
174
- `${baseURL}/user-assessment/${quizId}/${userId}/mark-quiz-complete`,
175
- {
176
- method: "POST",
177
- headers: {
178
- "Content-Type": "application/json",
179
- "Authorization": `Bearer ${token}`
180
- },
181
- }
182
- );
183
-
184
- if (response.ok) {
185
- handleQuizClose();
186
- await onQuizComplete();
187
- } else {
188
- // console.error("Failed to mark quiz complete");
189
- }
190
- } catch (error) {
191
- // console.error("Error marking quiz complete:", error);
192
- }
193
- };
194
-
195
- const handleNext = async () => {
196
- if (!selectedOption) return;
197
- setLoading(true);
198
-
199
- try {
200
- await submitAnswer(
201
- quizId,
202
- questions[currentStep].id,
203
- userId,
204
- selectedOption
205
- );
206
-
207
- if (currentStep === questions.length - 1) {
208
- await markQuizComplete();
209
- } else {
210
- setSelectedOption(null);
211
- const timeoutId = window?.setTimeout(() => {
212
- setCurrentStep((prevStep) => prevStep + 1);
213
- const resetScrollTimeoutId = window.setTimeout(() => {
214
- setHasManuallyScrolledToBottom(false);
215
- }, 150);
216
- timeoutIdsRef.current.push(resetScrollTimeoutId);
217
- }, 300);
218
- timeoutIdsRef.current.push(timeoutId);
219
- }
220
- } finally {
221
- const timeoutId = window?.setTimeout(() => {
222
- setLoading(false);
223
- }, 300);
224
- timeoutIdsRef.current.push(timeoutId);
225
- }
226
- };
227
-
228
- function buttonLabel() {
229
- if (loading) return "Submitting";
230
- return currentStep === questions.length - 1 ? "Submit" : "Next";
231
- }
232
-
233
- if (isQuizEnd) return null;
234
-
235
- return (
236
- <div className="quiz-page">
237
- <div className="quiz-page-container">
238
- <div className="quiz-ticker">
239
- <h3 className="question-count">
240
- Questions: {currentStep + 1}/{questions.length}
241
- </h3>
242
- <div className="quiz-ticker-right">
243
- <Timer
244
- markQuizComplete={markQuizComplete}
245
- duration={gameData.duration}
246
- isEndScreenOpen={isEndScreenOpen}
247
- />
248
- </div>
249
- </div>
250
- <div className="quiz-container" ref={quizContainerRef}>
251
- <div className="quiz-left">
252
- <Question question={questions[currentStep].text} />
253
- </div>
254
-
255
- <div className="quiz-right">
256
- <div className="quiz-right-head">
257
- <h3 className="choose-option">Choose the right option</h3>
258
- <div className="next-btn-container">
259
- <button
260
- className="next-btn"
261
- disabled={!selectedOption || loading}
262
- onClick={handleNext}
263
- >
264
- {buttonLabel()}
265
- </button>
266
- </div>
267
- <div className="mobile-next-btn">
268
- <button
269
- className="next-btn"
270
- disabled={!selectedOption || loading}
271
- onClick={handleNext}
272
- >
273
- {buttonLabel()}
274
- </button>
275
- </div>
276
- </div>
277
- <Options
278
- loading={loading}
279
- options={questions[currentStep].options}
280
- selectedOption={selectedOption}
281
- onSelect={setSelectedOption}
282
- />
283
- </div>
284
- </div>
285
- </div>
286
- <TabSwitchModal open={isModalOpen} onClose={resetModalState} />
287
- <QuizEndScreen open={isEndScreenOpen} onClose={resetModalState} />
288
- {isScrollable && (
289
- <button className="scroll-down-btn bounce" onClick={scrollToBottom}>
290
- <DownIconUrl />
291
- </button>
292
- )}
293
- </div>
294
- );
295
- };
296
-
297
- export default Quiz;
@@ -1,53 +0,0 @@
1
- import React, { useState, useEffect, useRef } from "react";
2
- import { TimerProps } from "../types";
3
- import TimerIcon from "../assets/Timer.svg";
4
- import "../styles/timer.css";
5
-
6
- const Timer = ({ duration, markQuizComplete, isEndScreenOpen }: TimerProps) => {
7
- const [timeLeft, setTimeLeft] = useState(duration);
8
-
9
- // Use the proper ref type (NodeJS.Timeout in TS/Node, number in plain JS)
10
- const timerRef = useRef<number | null>(null);
11
-
12
- // Start / restart when duration changes
13
- useEffect(() => {
14
- if (timerRef.current) clearInterval(timerRef.current);
15
- timerRef.current = setInterval(() => {
16
- setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
17
- }, 1000);
18
- return () => {
19
- if (timerRef.current) clearInterval(timerRef.current);
20
- };
21
- }, [duration]);
22
-
23
- // When time hits 0 → finish quiz
24
- useEffect(() => {
25
- if (timeLeft === 0) {
26
- markQuizComplete();
27
- if (timerRef.current) clearInterval(timerRef.current);
28
- }
29
- }, [timeLeft, markQuizComplete]);
30
-
31
- // If end‑screen opens early, stop the timer
32
- useEffect(() => {
33
- if (isEndScreenOpen && timerRef.current) {
34
- clearInterval(timerRef.current);
35
- }
36
- }, [isEndScreenOpen]);
37
-
38
- // Display mm:ss
39
- const formatTime = (seconds: number) => {
40
- const m = Math.floor(seconds / 60);
41
- const s = seconds % 60;
42
- return `${m}:${s < 10 ? "0" : ""}${s}`;
43
- };
44
-
45
- return (
46
- <div className="timer">
47
- <TimerIcon />
48
- <span>{formatTime(timeLeft)}</span>
49
- </div>
50
- );
51
- };
52
-
53
- export default Timer;
@@ -1,12 +0,0 @@
1
- import React from "react";
2
- import "../styles/loader.css";
3
-
4
- const Loader = () => {
5
- return (
6
- <div className="loader-overlay">
7
- <div className="loader-spinner"></div>
8
- </div>
9
- );
10
- };
11
-
12
- export default Loader;
@@ -1,20 +0,0 @@
1
- import React from "react";
2
- import { QuizEndScreenProps } from "../types";
3
- import "../styles/quizEndScreen.css";
4
-
5
- const QuizEndScreen: React.FC<QuizEndScreenProps> = ({ open, onClose }) => {
6
- if (!open) return null;
7
-
8
- return (
9
- <div className="end-screen-overlay">
10
- <div className="end-screen-content">
11
- {/* <button className="end-screen-close" onClick={onClose}>
12
- &times;
13
- </button> */}
14
- <p>Your quiz has ended due to switching tabs too many times.</p>
15
- </div>
16
- </div>
17
- );
18
- };
19
-
20
- export default QuizEndScreen;
@@ -1,30 +0,0 @@
1
- import React from "react";
2
- import { TabSwitchModalProps } from "../types";
3
- import "../styles/tabSwitchModal.css";
4
-
5
- const TabSwitchModal: React.FC<TabSwitchModalProps> = ({ open, onClose }) => {
6
- if (!open) return null;
7
-
8
- return (
9
- <div className="modal-overlay">
10
- <div className="modal-content">
11
- <button className="modal-close" onClick={onClose}>
12
- &times;
13
- </button>
14
- <div className="modal-header">
15
- <span className="modal-icon">⚠️</span>
16
- <h2>Tab Switch Detected</h2>
17
- </div>
18
- <p className="modal-text">
19
- Any further attempts to switch tabs or change browser windows will
20
- result in disqualification from the assessment.
21
- </p>
22
- <button className="modal-button" onClick={onClose}>
23
- Understood
24
- </button>
25
- </div>
26
- </div>
27
- );
28
- };
29
-
30
- export default TabSwitchModal;
@@ -1,25 +0,0 @@
1
- import { useEffect } from "react";
2
-
3
- const useDisableCopyPaste = () => {
4
- useEffect(() => {
5
- const disableCopy = (e: ClipboardEvent) => e.preventDefault();
6
- const disableSelect = (e: Event) => e.preventDefault();
7
- const disableRightClick = (e: MouseEvent) => e.preventDefault();
8
-
9
- document.addEventListener("copy", disableCopy);
10
- document.addEventListener("cut", disableCopy);
11
- document.addEventListener("paste", disableCopy);
12
- document.addEventListener("selectstart", disableSelect);
13
- document.addEventListener("contextmenu", disableRightClick);
14
-
15
- return () => {
16
- document.removeEventListener("copy", disableCopy);
17
- document.removeEventListener("cut", disableCopy);
18
- document.removeEventListener("paste", disableCopy);
19
- document.removeEventListener("selectstart", disableSelect);
20
- document.removeEventListener("contextmenu", disableRightClick);
21
- };
22
- }, []);
23
- };
24
-
25
- export default useDisableCopyPaste;
@@ -1,29 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- const useTabSwitch = (maxSwitches = 3) => {
4
- const [switchCount, setSwitchCount] = useState(0);
5
- const [isLimitReached, setIsLimitReached] = useState(false);
6
-
7
- useEffect(() => {
8
- const handleVisibilityChange = () => {
9
- if (document.hidden) {
10
- setSwitchCount((prev) => prev + 1);
11
- }
12
- };
13
-
14
- document.addEventListener("visibilitychange", handleVisibilityChange);
15
- return () => {
16
- document.removeEventListener("visibilitychange", handleVisibilityChange);
17
- };
18
- }, []);
19
-
20
- useEffect(() => {
21
- if (switchCount >= maxSwitches) {
22
- setIsLimitReached(true);
23
- }
24
- }, [switchCount, maxSwitches]);
25
-
26
- return { switchCount, isLimitReached };
27
- };
28
-
29
- export default useTabSwitch;
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- import QuizPlayer from "./components/Quiz";
2
-
3
- export default QuizPlayer;
@@ -1,44 +0,0 @@
1
- /* Thin (100) */
2
- @font-face {
3
- font-family: "Patron";
4
- src: url("../assets/fonts/patron/patron-thin.woff2") format("woff2");
5
- font-weight: 100;
6
- font-style: normal;
7
- font-display: swap;
8
- }
9
-
10
- /* Light (300) */
11
- @font-face {
12
- font-family: "Patron";
13
- src: url("../assets/fonts/patron/patron-light.woff2") format("woff2");
14
- font-weight: 300;
15
- font-style: normal;
16
- font-display: swap;
17
- }
18
-
19
- /* Regular (400) */
20
- @font-face {
21
- font-family: "Patron";
22
- src: url("../assets/fonts/patron/patron-regular.woff2") format("woff2");
23
- font-weight: 400;
24
- font-style: normal;
25
- font-display: swap;
26
- }
27
-
28
- /* Medium (500) */
29
- @font-face {
30
- font-family: "Patron";
31
- src: url("../assets/fonts/patron/patron-medium.woff2") format("woff2");
32
- font-weight: 500;
33
- font-style: normal;
34
- font-display: swap;
35
- }
36
-
37
- /* Bold (700) */
38
- @font-face {
39
- font-family: "Patron";
40
- src: url("../assets/fonts/patron/patron-bold.woff2") format("woff2");
41
- font-weight: 700;
42
- font-style: normal;
43
- font-display: swap;
44
- }
@@ -1,27 +0,0 @@
1
- .loader-overlay {
2
- position: fixed;
3
- top: 0;
4
- left: 0;
5
- width: 100%;
6
- height: 100%;
7
- background-color: black;
8
- display: flex;
9
- align-items: center;
10
- justify-content: center;
11
- z-index: 9999;
12
- }
13
-
14
- .loader-spinner {
15
- border: 8px solid rgba(255, 255, 255, 0.2);
16
- border-top: 8px solid white;
17
- border-radius: 50%;
18
- width: 60px;
19
- height: 60px;
20
- animation: spin 1s linear infinite;
21
- }
22
-
23
- @keyframes spin {
24
- to {
25
- transform: rotate(360deg);
26
- }
27
- }
@@ -1,58 +0,0 @@
1
- .options-container {
2
- display: flex;
3
- flex-direction: column;
4
- gap: 10px;
5
- }
6
-
7
- .option-btn {
8
- width: 100%;
9
- padding: 12px;
10
- border-radius: 8px;
11
- background: #ffffff;
12
- cursor: pointer;
13
- transition: 0.3s;
14
- color: #161c20;
15
- border: none;
16
- text-align: left;
17
- border: 1px solid #e7e7e7;
18
- }
19
-
20
- .option-btn:hover {
21
- background: #e1d8c9;
22
- }
23
-
24
- .option-btn.selected {
25
- background: #b59b7b;
26
- }
27
-
28
- .option-text {
29
- font-size: 1rem;
30
- font-weight: 500;
31
- line-height: 22px;
32
- letter-spacing: -0.1%;
33
- font-family: "Patron", sans-serif;
34
- }
35
-
36
- .option-image {
37
- max-width: 100%;
38
- height: auto;
39
- border-radius: 5px;
40
- }
41
-
42
- .option-code {
43
- background-color: #1e1e1e;
44
- color: white;
45
- padding: 8px;
46
- border-radius: 5px;
47
- }
48
-
49
- @media (max-width: 768px) {
50
- .option-text {
51
- font-size: 0.95rem;
52
- line-height: 21px;
53
- }
54
-
55
- .option-btn:hover {
56
- background: #b59b7b;
57
- }
58
- }
@@ -1,40 +0,0 @@
1
- /* .questionHtmlWrapper ul {
2
- list-style-type: disc;
3
- padding-left: 1.5rem;
4
- list-style-position: inside;
5
- }
6
-
7
- .questionHtmlWrapper ol {
8
- list-style-type: disc;
9
- padding-left: 1.5rem;
10
- list-style-position: inside;
11
- } */
12
-
13
- .question-text {
14
- font-size: 1.15rem;
15
- font-weight: 600;
16
- color: #161c20;
17
- font-family: "Patron", sans-serif;
18
- line-height: 25px;
19
- }
20
-
21
- code {
22
- font-size: 0.8rem;
23
- font-weight: normal;
24
- }
25
-
26
- @media (max-width: 1320px) {
27
- .question-text {
28
- font-size: 1rem;
29
- }
30
- .questionHtmlWrapper code {
31
- font-size: 0.8rem;
32
- }
33
- }
34
-
35
- @media (max-width: 768px) {
36
- .question-text {
37
- font-size: 0.95rem;
38
- line-height: 22px;
39
- }
40
- }