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.
- package/dist/index.js +0 -1
- package/dist/index.mjs +0 -1
- package/package.json +12 -4
- package/rollup.config-1745339193645.cjs +0 -50
- package/rollup.config.js +0 -45
- package/src/assets/Timer.svg +0 -3
- package/src/assets/close.svg +0 -4
- package/src/assets/downArrow.svg +0 -11
- package/src/assets/fonts/patron/patron-black.woff2 +0 -0
- package/src/assets/fonts/patron/patron-blackitalic.woff2 +0 -0
- package/src/assets/fonts/patron/patron-bold.woff2 +0 -0
- package/src/assets/fonts/patron/patron-bolditalic.woff2 +0 -0
- package/src/assets/fonts/patron/patron-italic.woff2 +0 -0
- package/src/assets/fonts/patron/patron-light.woff2 +0 -0
- package/src/assets/fonts/patron/patron-lightitalic.woff2 +0 -0
- package/src/assets/fonts/patron/patron-medium.woff2 +0 -0
- package/src/assets/fonts/patron/patron-mediumitalic.woff2 +0 -0
- package/src/assets/fonts/patron/patron-regular.woff2 +0 -0
- package/src/assets/fonts/patron/patron-thin.woff2 +0 -0
- package/src/assets/fonts/patron/patron-thinitalic.woff2 +0 -0
- package/src/assets/fonts/quintus/Quintus_B_trial.ttf +0 -0
- package/src/assets/fonts/quintus/Quintus_R_trial.ttf +0 -0
- package/src/assets/react.svg +0 -1
- package/src/components/Options.tsx +0 -27
- package/src/components/Question.tsx +0 -18
- package/src/components/Quiz.tsx +0 -297
- package/src/components/Timer.tsx +0 -53
- package/src/components/loader.tsx +0 -12
- package/src/components/quizEndScreen.tsx +0 -20
- package/src/components/tabSwitchModal.tsx +0 -30
- package/src/hooks/useDisableCopyPaste.ts +0 -25
- package/src/hooks/useTabSwitch.tsx +0 -29
- package/src/index.ts +0 -3
- package/src/styles/fonts.css +0 -44
- package/src/styles/loader.css +0 -27
- package/src/styles/options.css +0 -58
- package/src/styles/question.css +0 -40
- package/src/styles/quiz.css +0 -267
- package/src/styles/quizEndScreen.css +0 -33
- package/src/styles/tabSwitchModal.css +0 -78
- package/src/styles/timer.css +0 -5
- package/src/svg.d.ts +0 -6
- package/src/types/index.ts +0 -60
- package/tsconfig.json +0 -20
- package/wizr-quiz-1.0.0.tgz +0 -0
- package/wizr-sdk-1.0.0.tgz +0 -0
package/src/components/Quiz.tsx
DELETED
|
@@ -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;
|
package/src/components/Timer.tsx
DELETED
|
@@ -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,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
|
-
×
|
|
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
|
-
×
|
|
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
package/src/styles/fonts.css
DELETED
|
@@ -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
|
-
}
|
package/src/styles/loader.css
DELETED
|
@@ -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
|
-
}
|
package/src/styles/options.css
DELETED
|
@@ -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
|
-
}
|
package/src/styles/question.css
DELETED
|
@@ -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
|
-
}
|