Skip to content

Commit 54ed30b

Browse files
committed
[feat] add tpying and result page
1 parent 8b80f49 commit 54ed30b

File tree

9 files changed

+355
-194
lines changed

9 files changed

+355
-194
lines changed

src/App.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
scroll-behavior: smooth;
66
}
77
.word {
8-
@apply text-3xl font-bold text-gray-400 flex;
8+
@apply text-2xl font-extrabold text-gray-400 flex;
99
}
1010
.correct {
1111
@apply text-green-500;
@@ -14,7 +14,7 @@
1414
@apply text-red-500;
1515
}
1616
.typingBackgroundColor {
17-
background-color: hsl(220 20% 14%);
17+
background-color: var(--typing-background);
1818
}
1919
.caret {
2020
position: absolute;
@@ -34,3 +34,8 @@
3434
opacity: 1;
3535
}
3636
}
37+
.blurred {
38+
opacity: 0.25;
39+
filter: blur(4px);
40+
-webkit-filter: blur(4px);
41+
}

src/components/Typing/CameraComponent.tsx

Lines changed: 88 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,114 @@
11
import clsx from "clsx";
2-
import React, { useEffect, useRef, useState } from "react";
2+
import React, { useCallback, useEffect, useRef, useState } from "react";
33
import toast from "react-hot-toast";
44

5-
const CameraComponent = ({ isGameStarted }: { isGameStarted: boolean }) => {
5+
const CameraComponent = ({ isGameStarted ,countFaults}: { isGameStarted: boolean ,countFaults:()=> void}) => {
6+
const [socket, setSocket] = useState<WebSocket | null>(null);
67
const [isCheating, setIsCheating] = useState<boolean>(false);
78
const videoRef = useRef<HTMLVideoElement>(null);
9+
const getCamera = async () => {
10+
const stream=await navigator.mediaDevices.getUserMedia({ video: true });
11+
if (videoRef.current) {
12+
videoRef.current.srcObject = stream;
13+
videoRef.current.play();
14+
}
15+
return stream;
16+
};
17+
//when the game is started and socket exist , send the frame to the server
18+
const sendFrame=useCallback(function () {
19+
//used canvas to send blob object
20+
if (!videoRef.current) return;
21+
const canvas = document.createElement("canvas");
22+
canvas.width = videoRef?.current?.videoWidth || 0;
23+
canvas.height = videoRef?.current?.videoWidth || 0;
24+
const context = canvas.getContext("2d");
25+
context?.drawImage(
26+
videoRef.current,
27+
0,
28+
0,
29+
canvas.width,
30+
canvas.height
31+
);
32+
canvas.toBlob(function (blob) {
33+
if (blob && isGameStarted && socket?.readyState===1) {
34+
socket.send(blob);
35+
}
36+
}, "image/jpeg");
37+
},[socket,isGameStarted]);
38+
// function sendFrame() {
39+
// //used canvas to send blob object
40+
// if (!videoRef.current) return;
41+
// const canvas = document.createElement("canvas");
42+
// canvas.width = videoRef?.current?.videoWidth || 0;
43+
// canvas.height = videoRef?.current?.videoWidth || 0;
44+
// const context = canvas.getContext("2d");
45+
// context?.drawImage(
46+
// videoRef.current,
47+
// 0,
48+
// 0,
49+
// canvas.width,
50+
// canvas.height
51+
// );
52+
// canvas.toBlob(function (blob) {
53+
// if (blob && isGameStarted && socket?.readyState===1) {
54+
// socket.send(blob);
55+
// }
56+
// }, "image/jpeg");
57+
// }
58+
useEffect(() => {
59+
//used to connect to the websocket server once the component is mounted
60+
const websocket = new WebSocket(import.meta.env.VITE_WS_URL as string);
61+
setSocket(websocket);
62+
websocket.onopen = () => {
63+
console.log("Websocket connected");
64+
};
65+
websocket.onerror = () => {
66+
toast.error("Error in socket connection");
67+
};
68+
}, []);
869
useEffect(() => {
70+
console.log("isGameStarted", isGameStarted);
971
try {
10-
let websocket: WebSocket;
11-
if (isGameStarted) {
12-
websocket = new WebSocket("ws://localhost:8765");
13-
websocket.onopen = () => {
14-
toast.success("Video is being monitored, don't cheat!");
15-
};
16-
websocket.onerror = () => {
17-
toast.error("Error in websocket connection");
18-
};
19-
websocket.onmessage = () => {
20-
toast.error("Keyboard me mat dekh", {
72+
if (isGameStarted && socket) {
73+
74+
socket.onmessage = () => {
75+
toast.error("Looking at Keyboard ! Points will be reduced", {
2176
position: "top-center",
2277
});
78+
countFaults();
2379
setIsCheating(true);
24-
setTimeout(() => {
80+
const id = setTimeout(() => {
2581
setIsCheating(false);
2682
}, 500);
83+
if (!isCheating) {
84+
clearTimeout(id);
85+
}
2786
};
2887
}
29-
if (navigator.mediaDevices.getUserMedia) {
30-
navigator.mediaDevices
31-
.getUserMedia({ video: true })
32-
.then(function (stream) {
33-
if (videoRef.current) {
34-
videoRef.current.srcObject = stream;
35-
if (isGameStarted) websocket.binaryType = "arraybuffer";
36-
videoRef.current.play();
37-
//send the stream to the server every 0.1s
38-
videoRef.current.addEventListener("play", function () {
39-
if (isGameStarted) setInterval(sendFrame, 100);
40-
});
41-
}
42-
function sendFrame() {
43-
//used canvas to send blob object
44-
if (!videoRef.current) return;
45-
const canvas = document.createElement("canvas");
46-
canvas.width = videoRef?.current?.videoWidth || 0;
47-
canvas.height = videoRef?.current?.videoWidth || 0;
48-
const context = canvas.getContext("2d");
49-
context?.drawImage(
50-
videoRef.current,
51-
0,
52-
0,
53-
canvas.width,
54-
canvas.height
55-
);
56-
canvas.toBlob(function (blob) {
57-
if (blob) {
58-
websocket.send(blob);
59-
}
60-
}, "image/jpeg");
61-
}
62-
})
63-
.catch(function (error) {
64-
console.log("Something went wrong with webcam access: ", error);
65-
});
88+
getCamera();
89+
if(videoRef.current && socket){
90+
socket.binaryType = "arraybuffer";
91+
videoRef.current.addEventListener("play", function () {
92+
const id=setInterval(sendFrame, 100);
93+
if(!isGameStarted) clearInterval(id);
94+
});
6695
}
6796
} catch (e) {
68-
console.log(e);
97+
console.log("Error in webcam access: ", e);
6998
}
99+
return () => {
100+
if (socket) socket.close();
101+
};
70102
}, [isGameStarted]);
71103
return (
72104
<div>
105+
<p className="text-red-600 font-extrabold text-xl h-7">
106+
{isCheating && "Don't Look at keyboard!"}
107+
</p>
73108
<video
74109
ref={videoRef}
75110
className={clsx(
76-
"absolute bottom-0 right-0 w-[300px] h-auto border-2 border-white",
111+
"absolute bottom-0 right-0 w-[250px] h-auto border-2 border-white",
77112
isCheating && "border-red-500 object-cover"
78113
)}
79114
></video>

src/components/Typing/Control.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import React from "react";
22

33
const Control = () => {
44
return (
5-
<nav className="w-full flex flex-col gap-5 p-3 sm:flex-row sm:justify-between sm:p-5 shadow-2xl rounded-xl border-2 text-white">
6-
<h1 className="font-bold text-3xl">TypeSight</h1>
5+
<nav className="w-full flex flex-col gap-5 p-3 sm:flex-row sm:justify-between sm:p-5 shadow-2xl rounded-xl text-white bg-subColor">
6+
<h1 className="font-bold text-3xl flex items-center gap-2">
7+
<img src="/logo.png" alt="Typesight Logo" className="h-10 w-10" />
8+
TypeSight
9+
</h1>
710
<div className="flex justify-end gap-5">
8-
<select className="px-3 rounded-xl bg-white text-black">
11+
{/* <select className="px-3 rounded-xl bg-white text-black">
912
<option>30s</option>
1013
<option>60s</option>
11-
</select>
12-
<select className="px-3 rounded-xl bg-white text-black">
14+
</select> */}
15+
{/* <select className="px-3 rounded-xl bg-white text-black">
1316
<option>words</option>
1417
<option>sentences</option>
15-
</select>
18+
</select> */}
1619
</div>
1720
</nav>
1821
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import useAppDispatch from "@/hooks/useAppDispatch";
2+
import { addResultIfo } from "@/redux-store/slices/resultslice";
3+
import React, { useEffect } from "react";
4+
const data={
5+
duration:30,
6+
score:0,
7+
accuracy:0
8+
}
9+
const TypingResult = ({ wpm, accuracy ,faults}: { wpm: number; accuracy: number ,faults:number}) => {
10+
const dispatch=useAppDispatch();
11+
// update the data to the database
12+
function updateData() {
13+
data.score = wpm;
14+
data.accuracy = accuracy;
15+
if (import.meta.env.VITE_APP_ENV === "development") console.log(data);
16+
dispatch(addResultIfo(data));
17+
}
18+
useEffect(() => {
19+
updateData();
20+
}, []);
21+
return (
22+
<div className="w-full text-center flex flex-col gap-5 ">
23+
<Wrapper>
24+
<Heading text="Wpm" />
25+
<Result> {wpm} <span className="text-3xl">wpm</span></Result>
26+
</Wrapper>
27+
<Wrapper>
28+
<Heading text="Accuracy" />
29+
<Result>{accuracy} <span className="text-3xl">%</span></Result>
30+
</Wrapper>
31+
<Wrapper>
32+
<Heading text="Faults" />
33+
<Result>{faults} <span className="text-3xl">%</span></Result>
34+
</Wrapper>
35+
</div>
36+
);
37+
};
38+
function Wrapper({ children }: { children: React.ReactNode }) {
39+
return <div>{children}</div>;
40+
}
41+
function Heading({ text }: { text: string }) {
42+
return (
43+
<h1 className="text-lg sm:text-4xl font-bold text-typingText">{text}</h1>
44+
);
45+
}
46+
function Result({ children}: {children:React.ReactNode}) {
47+
return <span className="text-2xl sm:text-6xl font-bold text-mainColor">{children}</span>;
48+
}
49+
50+
export default TypingResult;

src/index.css

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,72 +5,76 @@
55
@layer base {
66
:root {
77
--background: 0 0% 100%;
8+
--typing-background: #323437;
9+
--typing-text: #646669;
10+
--main-color:#e2b714;
11+
--typing-navbar: #2c2e31;
812
--foreground: 20 14.3% 4.1%;
913

1014
--card: 0 0% 100%;
1115
--card-foreground: 20 14.3% 4.1%;
12-
16+
1317
--popover: 0 0% 100%;
1418
--popover-foreground: 20 14.3% 4.1%;
15-
19+
1620
--primary: 24 9.8% 10%;
1721
--primary-foreground: 60 9.1% 97.8%;
18-
22+
1923
--secondary: 60 4.8% 95.9%;
2024
--secondary-foreground: 24 9.8% 10%;
21-
25+
2226
--muted: 60 4.8% 95.9%;
2327
--muted-foreground: 25 5.3% 44.7%;
24-
28+
2529
--accent: 60 4.8% 95.9%;
2630
--accent-foreground: 24 9.8% 10%;
27-
31+
2832
--destructive: 0 84.2% 60.2%;
2933
--destructive-foreground: 60 9.1% 97.8%;
3034

3135
--border: 20 5.9% 90%;
3236
--input: 20 5.9% 90%;
3337
--ring: 20 14.3% 4.1%;
34-
38+
3539
--radius: 0.5rem;
3640
}
37-
41+
3842
.dark {
3943
--background: 20 14.3% 4.1%;
4044
--foreground: 60 9.1% 97.8%;
41-
45+
4246
--card: 20 14.3% 4.1%;
4347
--card-foreground: 60 9.1% 97.8%;
44-
48+
4549
--popover: 20 14.3% 4.1%;
4650
--popover-foreground: 60 9.1% 97.8%;
47-
51+
4852
--primary: 60 9.1% 97.8%;
4953
--primary-foreground: 24 9.8% 10%;
50-
54+
5155
--secondary: 12 6.5% 15.1%;
5256
--secondary-foreground: 60 9.1% 97.8%;
53-
57+
5458
--muted: 12 6.5% 15.1%;
5559
--muted-foreground: 24 5.4% 63.9%;
56-
60+
5761
--accent: 12 6.5% 15.1%;
5862
--accent-foreground: 60 9.1% 97.8%;
59-
63+
6064
--destructive: 0 62.8% 30.6%;
6165
--destructive-foreground: 60 9.1% 97.8%;
62-
66+
6367
--border: 12 6.5% 15.1%;
6468
--input: 12 6.5% 15.1%;
6569
--ring: 24 5.7% 82.9%;
6670
}
6771
}
68-
72+
6973
@layer base {
7074
* {
7175
@apply border-border;
7276
}
7377
body {
7478
@apply bg-background text-foreground;
7579
}
76-
}
80+
}

0 commit comments

Comments
 (0)