Skip to content

Commit 8b80f49

Browse files
committed
basic typing test without time and camera input ser
1 parent 1b75211 commit 8b80f49

File tree

11 files changed

+406
-19
lines changed

11 files changed

+406
-19
lines changed

.husky/pre-push

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npm run build

src/App.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,33 @@
44
box-sizing: border-box;
55
scroll-behavior: smooth;
66
}
7+
.word {
8+
@apply text-3xl font-bold text-gray-400 flex;
9+
}
10+
.correct {
11+
@apply text-green-500;
12+
}
13+
.wrong {
14+
@apply text-red-500;
15+
}
16+
.typingBackgroundColor {
17+
background-color: hsl(220 20% 14%);
18+
}
19+
.caret {
20+
position: absolute;
21+
height: 1.8rem;
22+
top: 0;
23+
border-right: 3px solid #e7d105;
24+
animation: caret 1s infinite;
25+
transition: all 0.2s ease;
26+
}
27+
28+
@keyframes caret {
29+
0%,
30+
to {
31+
opacity: 0;
32+
}
33+
50% {
34+
opacity: 1;
35+
}
36+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import clsx from "clsx";
2+
import React, { useEffect, useRef, useState } from "react";
3+
import toast from "react-hot-toast";
4+
5+
const CameraComponent = ({ isGameStarted }: { isGameStarted: boolean }) => {
6+
const [isCheating, setIsCheating] = useState<boolean>(false);
7+
const videoRef = useRef<HTMLVideoElement>(null);
8+
useEffect(() => {
9+
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", {
21+
position: "top-center",
22+
});
23+
setIsCheating(true);
24+
setTimeout(() => {
25+
setIsCheating(false);
26+
}, 500);
27+
};
28+
}
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+
});
66+
}
67+
} catch (e) {
68+
console.log(e);
69+
}
70+
}, [isGameStarted]);
71+
return (
72+
<div>
73+
<video
74+
ref={videoRef}
75+
className={clsx(
76+
"absolute bottom-0 right-0 w-[300px] h-auto border-2 border-white",
77+
isCheating && "border-red-500 object-cover"
78+
)}
79+
></video>
80+
</div>
81+
);
82+
};
83+
84+
const MemoizedCameraComponent = React.memo(CameraComponent);
85+
export default MemoizedCameraComponent;

src/components/Typing/Control.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ 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 border-gray-400">
6-
<h1>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 border-2 text-white">
6+
<h1 className="font-bold text-3xl">TypeSight</h1>
77
<div className="flex justify-end gap-5">
8-
<select className="px-3 rounded-xl bg-gray-200">
8+
<select className="px-3 rounded-xl bg-white text-black">
99
<option>30s</option>
1010
<option>60s</option>
1111
</select>
12-
<select className="px-3 rounded-xl bg-gray-200">
12+
<select className="px-3 rounded-xl bg-white text-black">
1313
<option>words</option>
1414
<option>sentences</option>
1515
</select>

src/components/Typing/Typing.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
1-
import React from "react";
1+
import React, { useEffect, useRef, useState } from "react";
2+
import toast from "react-hot-toast";
23
type TypeProps = {
34
text: string;
45
};
56
const Typing = ({ text }: TypeProps) => {
6-
return <div>
7-
{text}
8-
</div>;
7+
const ref=useRef<HTMLInputElement>(null);
8+
const [time, settime] = useState<number>(30);
9+
const [istimercompleted, setcomlete] = useState<boolean>(false);
10+
useEffect(() => {
11+
const id = setInterval(() => {
12+
if (!istimercompleted) settime((prev) => prev - 1);
13+
}, 1000);
14+
if (istimercompleted) {
15+
clearInterval(id);
16+
settime(30);
17+
toast.success("Times Up");
18+
}
19+
return () => clearInterval(id);
20+
}, [istimercompleted]);
21+
useEffect(() => {
22+
if (time === -1) setcomlete(true);
23+
}, [time]);
24+
useEffect(() => {
25+
ref.current?.focus();
26+
}, []);
27+
return (
28+
<div className="w-full relative">
29+
<input type="text" className="absolute top-10 opacity-0" onChange={()=>{
30+
console.log("Pressed something")
31+
}} ref={ref}/>
32+
<div className="text-2xl font-extrabold text-green-400">{time}s</div>
33+
<div className="text-justify text-3xl font-bold leading-10 text-gray-500">
34+
{text}
35+
</div>
36+
</div>
37+
);
938
};
1039

1140
export default Typing;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
3+
const TypingFooter = () => {
4+
return (
5+
<div className="w-full flex justify-center">
6+
<button>Restart test</button>
7+
</div>
8+
);
9+
};
10+
11+
export default TypingFooter;

src/components/WrongWord.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react'
2+
3+
const WrongWord = ({letter}:{letter:string}) => {
4+
return (
5+
<span className='wrong'>
6+
{letter}
7+
</span>
8+
)
9+
}
10+
11+
export default WrongWord

src/context/TypingContext.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { ReactNode } from 'react'
2+
interface Props{
3+
children:ReactNode
4+
}
5+
const TypingContext = ({children}:Props) => {
6+
return (
7+
<div>
8+
{children}
9+
</div>
10+
)
11+
}
12+
13+
export default TypingContext

src/lib/helpers/shufflewords.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import sentence from '@/lib/data/sentences.json';
44
export default function shuffleWords(preference: string) {
55
switch (preference) {
66
case 'words':
7-
console.log(_.shuffle(word).slice(0, 100));
8-
9-
return _.shuffle(word).slice(0, 100);
7+
return _.shuffle(word).slice(0, 50);
108
case 'sentences':
11-
return _.shuffle(sentence).slice(0, 100);
9+
return _.shuffle(sentence).slice(0, 50);
1210
default:
13-
return _.shuffle(word).slice(0, 100);
11+
return _.shuffle(word).slice(0, 50);
1412
}
1513
}

0 commit comments

Comments
 (0)