wizr-quiz 1.0.0 → 2.0.0
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.d.ts +102 -1
- package/dist/index.js +1 -11
- package/dist/index.mjs +1 -11
- package/package.json +17 -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/rollup.config.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import resolve from "@rollup/plugin-node-resolve";
|
|
2
|
-
import commonjs from "@rollup/plugin-commonjs";
|
|
3
|
-
import typescript from "@rollup/plugin-typescript";
|
|
4
|
-
import dts from "rollup-plugin-dts";
|
|
5
|
-
import terser from "@rollup/plugin-terser";
|
|
6
|
-
import peerDepsExternal from "rollup-plugin-peer-deps-external";
|
|
7
|
-
import svgr from "@svgr/rollup";
|
|
8
|
-
|
|
9
|
-
import postcss from "rollup-plugin-postcss";
|
|
10
|
-
|
|
11
|
-
const packageJson = require("./package.json");
|
|
12
|
-
|
|
13
|
-
export default [
|
|
14
|
-
{
|
|
15
|
-
input: "src/index.ts",
|
|
16
|
-
output: [
|
|
17
|
-
{
|
|
18
|
-
file: packageJson.main,
|
|
19
|
-
format: "cjs",
|
|
20
|
-
sourcemap: true,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
file: packageJson.module,
|
|
24
|
-
format: "esm",
|
|
25
|
-
sourcemap: true,
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
plugins: [
|
|
29
|
-
peerDepsExternal(),
|
|
30
|
-
resolve(),
|
|
31
|
-
commonjs(),
|
|
32
|
-
typescript({ tsconfig: "./tsconfig.json" }),
|
|
33
|
-
terser(),
|
|
34
|
-
svgr(),
|
|
35
|
-
postcss(),
|
|
36
|
-
],
|
|
37
|
-
external: ["react", "react-dom"],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
input: "src/index.ts",
|
|
41
|
-
output: [{ file: packageJson.types }],
|
|
42
|
-
plugins: [dts.default()],
|
|
43
|
-
external: [/\.css$/],
|
|
44
|
-
},
|
|
45
|
-
];
|
package/src/assets/Timer.svg
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<path d="M8 2.5C6.81331 2.5 5.65328 2.85189 4.66658 3.51118C3.67989 4.17047 2.91085 5.10754 2.45673 6.2039C2.0026 7.30026 1.88378 8.50665 2.11529 9.67054C2.3468 10.8344 2.91825 11.9035 3.75736 12.7426C4.59648 13.5818 5.66558 14.1532 6.82946 14.3847C7.99335 14.6162 9.19975 14.4974 10.2961 14.0433C11.3925 13.5892 12.3295 12.8201 12.9888 11.8334C13.6481 10.8467 14 9.68669 14 8.5C13.9982 6.90926 13.3655 5.38419 12.2406 4.25937C11.1158 3.13454 9.59074 2.50182 8 2.5ZM10.8538 6.35375L8.35375 8.85375C8.3073 8.9002 8.25215 8.93705 8.19145 8.9622C8.13075 8.98734 8.0657 9.00028 8 9.00028C7.93431 9.00028 7.86925 8.98734 7.80855 8.9622C7.74786 8.93705 7.69271 8.9002 7.64625 8.85375C7.5998 8.80729 7.56295 8.75214 7.53781 8.69145C7.51266 8.63075 7.49972 8.5657 7.49972 8.5C7.49972 8.4343 7.51266 8.36925 7.53781 8.30855C7.56295 8.24785 7.5998 8.1927 7.64625 8.14625L10.1463 5.64625C10.1927 5.59979 10.2479 5.56294 10.3086 5.5378C10.3693 5.51266 10.4343 5.49972 10.5 5.49972C10.5657 5.49972 10.6308 5.51266 10.6915 5.5378C10.7521 5.56294 10.8073 5.59979 10.8538 5.64625C10.9002 5.6927 10.9371 5.74786 10.9622 5.80855C10.9873 5.86925 11.0003 5.9343 11.0003 6C11.0003 6.0657 10.9873 6.13075 10.9622 6.19145C10.9371 6.25214 10.9002 6.30729 10.8538 6.35375ZM6 1C6 0.867392 6.05268 0.740215 6.14645 0.646447C6.24022 0.552678 6.36739 0.5 6.5 0.5H9.5C9.63261 0.5 9.75979 0.552678 9.85356 0.646447C9.94732 0.740215 10 0.867392 10 1C10 1.13261 9.94732 1.25979 9.85356 1.35355C9.75979 1.44732 9.63261 1.5 9.5 1.5H6.5C6.36739 1.5 6.24022 1.44732 6.14645 1.35355C6.05268 1.25979 6 1.13261 6 1Z" fill="#1D1B21"/>
|
|
3
|
-
</svg>
|
package/src/assets/close.svg
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<rect width="34" height="34" rx="17" fill="#EEEAE8"/>
|
|
3
|
-
<path d="M21.9998 12C21.8436 11.8438 21.6317 11.756 21.4107 11.756C21.1897 11.756 20.9778 11.8438 20.8215 12L16.9998 15.8217L13.1782 12C13.0219 11.8438 12.81 11.756 12.589 11.756C12.368 11.756 12.1561 11.8438 11.9998 12C11.8436 12.1563 11.7559 12.3682 11.7559 12.5892C11.7559 12.8101 11.8436 13.0221 11.9998 13.1783L15.8215 17L11.9998 20.8217C11.8436 20.9779 11.7559 21.1899 11.7559 21.4108C11.7559 21.6318 11.8436 21.8437 11.9998 22C12.1561 22.1562 12.368 22.244 12.589 22.244C12.81 22.244 13.0219 22.1562 13.1782 22L16.9998 18.1783L20.8215 22C20.9778 22.1562 21.1897 22.244 21.4107 22.244C21.6317 22.244 21.8436 22.1562 21.9998 22C22.1561 21.8437 22.2438 21.6318 22.2438 21.4108C22.2438 21.1899 22.1561 20.9779 21.9998 20.8217L18.1782 17L21.9998 13.1783C22.1561 13.0221 22.2438 12.8101 22.2438 12.5892C22.2438 12.3682 22.1561 12.1563 21.9998 12Z" fill="black"/>
|
|
4
|
-
</svg>
|
package/src/assets/downArrow.svg
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<rect width="38" height="38" rx="19" fill="#151515"/>
|
|
3
|
-
<g clip-path="url(#clip0_3814_2711)">
|
|
4
|
-
<path d="M8.75781 21.1938C8.79531 21.3344 8.90312 21.5547 8.99219 21.686C9.08125 21.8219 11.1297 23.9078 13.5484 26.3219C18.5828 31.3516 18.1422 30.9766 19 30.9766C19.4266 30.9766 19.525 30.9625 19.7266 30.8641C19.9094 30.7703 20.9078 29.8047 24.4516 26.2703C26.9219 23.8047 28.9984 21.7 29.0641 21.5922C29.2094 21.3485 29.3125 20.9688 29.3125 20.6828C29.3125 20.2282 29.0172 19.6047 28.675 19.3469C28.3328 19.0844 27.7047 18.9485 27.2781 19.0469C26.7437 19.1688 26.7016 19.2063 23.9594 21.9203C21.2922 24.5688 21.0812 24.8032 20.8422 25.3797C20.7625 25.5813 20.7578 25.3375 20.7344 16.9375L20.7109 8.2891L20.5844 8.02191C20.4203 7.66566 20.1391 7.37503 19.7875 7.18285C19.5062 7.02816 19.4687 7.02347 19 7.02347C18.5547 7.02347 18.4844 7.03753 18.25 7.15472C17.7859 7.39378 17.4625 7.78753 17.3312 8.27503C17.275 8.48597 17.2656 9.88285 17.2656 17.0313L17.2656 25.5391L17.1297 25.2578C17.0594 25.1032 16.9 24.85 16.7828 24.6953C16.6656 24.5407 15.4234 23.2703 14.0219 21.8782C12.0766 19.9375 11.4203 19.3094 11.2422 19.2203C10.3281 18.7657 9.30156 19.0891 8.86562 19.9703C8.6875 20.3313 8.64531 20.8 8.75781 21.1938Z" fill="white"/>
|
|
5
|
-
</g>
|
|
6
|
-
<defs>
|
|
7
|
-
<clipPath id="clip0_3814_2711">
|
|
8
|
-
<rect width="24" height="24" fill="white" transform="translate(7 31) rotate(-90)"/>
|
|
9
|
-
</clipPath>
|
|
10
|
-
</defs>
|
|
11
|
-
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/assets/react.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { OptionsProps } from "../types";
|
|
3
|
-
import "../styles/options.css";
|
|
4
|
-
|
|
5
|
-
const Options = ({
|
|
6
|
-
options,
|
|
7
|
-
selectedOption,
|
|
8
|
-
onSelect,
|
|
9
|
-
loading,
|
|
10
|
-
}: OptionsProps) => {
|
|
11
|
-
return (
|
|
12
|
-
<div className="options-container">
|
|
13
|
-
{options.map((option) => (
|
|
14
|
-
<button
|
|
15
|
-
disabled={loading}
|
|
16
|
-
key={option.id}
|
|
17
|
-
className={`option-btn ${selectedOption === option.id && "selected"}`}
|
|
18
|
-
onClick={() => onSelect(option.id)}
|
|
19
|
-
>
|
|
20
|
-
{option.text && <span className="option-text">{option.text}</span>}
|
|
21
|
-
</button>
|
|
22
|
-
))}
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default Options;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { QuestionProps } from "../types";
|
|
3
|
-
import "../styles/question.css";
|
|
4
|
-
|
|
5
|
-
const Question = ({ question }: QuestionProps) => {
|
|
6
|
-
return (
|
|
7
|
-
<div className="question-html-wrapper" >
|
|
8
|
-
<h2
|
|
9
|
-
className="question-text"
|
|
10
|
-
dangerouslySetInnerHTML={{
|
|
11
|
-
__html: question,
|
|
12
|
-
}}
|
|
13
|
-
/>
|
|
14
|
-
</div>
|
|
15
|
-
);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default Question;
|
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