Skip to content

Commit 56148ba

Browse files
authored
Merge pull request #273 from sudo-adduser-jordan/main
[FEATURE]: Allow backspace on typing #185
2 parents d3be809 + c4550b0 commit 56148ba

File tree

7 files changed

+360
-335
lines changed

7 files changed

+360
-335
lines changed

src/app/race/displayed-code.tsx renamed to src/app/race/Code.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
11
import { cn } from "@/lib/utils";
2-
3-
interface displayCodeProps {
2+
interface CodeProps {
43
code: string;
54
userInput: string;
65
errors: number[];
7-
isCurrentLineEmpty: boolean;
86
}
9-
10-
export default function DisplayedCode({
11-
code,
12-
errors,
13-
userInput,
14-
isCurrentLineEmpty = false,
15-
}: displayCodeProps) {
7+
export default function Code({ code, errors, userInput }: CodeProps) {
168
return (
17-
<pre className="mb-4 text-primary">
9+
<pre className="text-primary mb-4 overflow-auto">
1810
{code.split("").map((char, index) => (
1911
<span
2012
key={index}
2113
className={cn({
22-
"bg-red-500 opacity-100": errors.includes(index),
23-
"animate-blink": userInput.length === index,
14+
"text-red-500 opacity-100": errors.includes(index),
15+
"bg-yellow-200 opacity-80 text-black": userInput.length === index,
2416
"opacity-100":
2517
userInput.length !== index && userInput[index] === char,
2618
"opacity-50":
2719
userInput.length !== index && userInput[index] !== char,
2820
})}
2921
>
30-
{isCurrentLineEmpty && userInput.length === index ? " \n" : char}
22+
{char}
3123
</span>
3224
))}
3325
</pre>

src/app/race/Race.tsx

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
"use client";
2+
3+
import React, { useState, useEffect, useRef } from "react";
4+
import type { User } from "next-auth";
5+
import { Button } from "@/components/ui/button";
6+
import { useRouter } from "next/navigation";
7+
import { Snippet } from "@prisma/client";
8+
import {
9+
Tooltip,
10+
TooltipContent,
11+
TooltipProvider,
12+
TooltipTrigger,
13+
} from "@/components/ui/tooltip";
14+
import {
15+
calculateAccuracy,
16+
calculateCPM,
17+
createIndent,
18+
calculateRemainder,
19+
previousLines,
20+
} from "./utils";
21+
22+
import { Heading } from "@/components/ui/heading";
23+
import { saveUserResultAction } from "../_actions/user";
24+
import RaceTracker from "./RaceTracker";
25+
import Code from "./Code";
26+
27+
interface RaceProps {
28+
user?: User;
29+
snippet: Snippet;
30+
}
31+
32+
export default function Race({ user, snippet }: RaceProps) {
33+
const [startTime, setStartTime] = useState<Date | null>(null);
34+
const [endTime, setEndTime] = useState<Date | null>(null);
35+
const [input, setInput] = useState("");
36+
37+
const [counter, setCounter] = useState(0);
38+
const [line, setLine] = useState(0);
39+
40+
const [errors, setErrors] = useState<number[]>([]);
41+
const [errorTotal, setErrorTotal] = useState(0);
42+
43+
const router = useRouter();
44+
45+
const inputElement = useRef<HTMLInputElement | null>(null);
46+
const code = snippet.code.trimEnd(); // remove trailing "\n"
47+
const lines = code.split("\n");
48+
49+
useEffect(() => {
50+
// Debug
51+
// console.log(JSON.stringify(input));
52+
// console.log(JSON.stringify(code));
53+
console.log(input.length);
54+
console.log(code.length);
55+
// console.log("Index: " + index);
56+
// console.log("Line Index: " + lineIndex);
57+
// console.log("Line Number: " + line);
58+
// console.log(lines[line]);
59+
60+
// Focus element
61+
if (inputElement.current !== null) {
62+
inputElement.current.focus();
63+
}
64+
65+
// Calculate result
66+
if (input.length === code.length && input === code) {
67+
setEndTime(new Date());
68+
}
69+
if (startTime && endTime) {
70+
const timeTaken = (endTime.getTime() - startTime.getTime()) / 1000;
71+
72+
// If logged in
73+
if (user)
74+
saveUserResultAction({
75+
timeTaken,
76+
// errors: errorTotal,
77+
errors: errors.length,
78+
cpm: calculateCPM(code.length - 1, timeTaken),
79+
accuracy: calculateAccuracy(code.length - 1, errorTotal),
80+
snippetId: snippet.id,
81+
});
82+
83+
router.push("/result");
84+
}
85+
86+
// Set Errors
87+
setErrors(() => {
88+
const currentText = code.substring(0, input.length);
89+
const newErrors = Array.from(input)
90+
.map((char, index) => (char !== currentText[index] ? index : -1))
91+
.filter((index) => index !== -1);
92+
return newErrors;
93+
});
94+
}, [
95+
endTime,
96+
startTime,
97+
user,
98+
errors.length,
99+
errorTotal,
100+
code.length,
101+
router,
102+
snippet.id,
103+
input,
104+
code,
105+
]);
106+
107+
// Reset Race
108+
useEffect(() => {
109+
const handleRestartKey = (e: KeyboardEvent) => {
110+
if (e.key === "Escape") {
111+
handleRestart();
112+
}
113+
};
114+
document.addEventListener("keydown", handleRestartKey);
115+
}, []);
116+
117+
// Focus On Load
118+
function focusOnLoad() {
119+
if (inputElement.current !== null) {
120+
inputElement.current.focus();
121+
}
122+
}
123+
124+
// Key Events - Enabled / Disable / Support Func
125+
function handleKeyboardEvent(e: React.KeyboardEvent<HTMLInputElement>) {
126+
setStartTime(new Date());
127+
128+
if (e.key === "Backspace") {
129+
Backspace();
130+
} else if (e.key === "Enter") {
131+
Enter();
132+
} else if (e.key === "Shift") {
133+
e.preventDefault();
134+
} else if (e.key === "Alt") {
135+
e.preventDefault();
136+
} else if (e.key === "ArrowUp") {
137+
// plug in arrow keys here
138+
e.preventDefault();
139+
} else if (e.key === "ArrowDown") {
140+
// plug in arrow keys here
141+
e.preventDefault();
142+
} else if (e.key === "ArrowRight") {
143+
// plug in arrow keys here
144+
e.preventDefault();
145+
} else if (e.key === "ArrowLeft") {
146+
// plug in arrow keys here
147+
e.preventDefault();
148+
} else if (e.key === "Control") {
149+
e.preventDefault();
150+
} else if (e.key === "Escape") {
151+
e.preventDefault();
152+
} else {
153+
Key(e);
154+
// Check Errors here
155+
}
156+
}
157+
158+
// Backspace
159+
function Backspace() {
160+
const ln = lines[line];
161+
const nextLine = lines[line - 1];
162+
const indent = createIndent(ln);
163+
const array = input.split("");
164+
165+
if (array.lastIndexOf("\n") == input.length - 1) {
166+
setInput(input.slice(0, -2));
167+
if (line != 0) {
168+
setLine(line - 1);
169+
}
170+
if (counter != 0) {
171+
setCounter(indent.length - ln.length);
172+
}
173+
} else if (array.lastIndexOf("\n") == input.length - indent.length - 1) {
174+
setInput(input.slice(0, -2 - indent.length));
175+
if (line != 0) {
176+
setLine(line - 1);
177+
}
178+
setCounter(nextLine.length - 1);
179+
} else {
180+
setInput(input.slice(0, -1));
181+
182+
if (counter != 0) {
183+
setCounter(counter - 1);
184+
}
185+
}
186+
}
187+
188+
// Enter
189+
function Enter() {
190+
// Stop at end of line if not matching
191+
if (input.length - 1 === code.length) {
192+
return;
193+
}
194+
195+
if (line < lines.length) {
196+
const ln = lines[line];
197+
const nextLine = lines[line + 1];
198+
const remainder = calculateRemainder(counter, ln);
199+
const indent = createIndent(nextLine);
200+
201+
if (line < lines.length - 1) {
202+
setInput(input + remainder + indent);
203+
setLine(line + 1);
204+
}
205+
206+
setCounter(indent.length);
207+
setErrors(() => {
208+
const currentText = code.slice(0, (input + remainder + indent).length);
209+
const newErrors = Array.from(input + remainder + indent)
210+
.map((char, index) => (char !== currentText[index] ? index : -1))
211+
.filter((index) => index !== -1);
212+
return newErrors;
213+
});
214+
}
215+
}
216+
217+
// Default
218+
function Key(e: React.KeyboardEvent<HTMLInputElement>) {
219+
const ln = lines[line];
220+
const nextLine = lines[line + 1];
221+
const indent = createIndent(nextLine);
222+
223+
// Stop at end of line if not matching
224+
if (input.length - 1 === code.length) {
225+
return;
226+
}
227+
228+
if (counter == ln.length - 1) {
229+
setCounter(indent.length);
230+
setInput(input + e.key + "\n" + indent);
231+
if (line < lines.length - 1) {
232+
setLine(line + 1);
233+
}
234+
} else if (ln.length - 1 == counter - previousLines(lines, line).length) {
235+
setCounter(indent.length);
236+
setInput(input + e.key + "\n" + indent);
237+
if (line < lines.length - 1) {
238+
setLine(line + 1);
239+
}
240+
} else {
241+
setInput(input + e.key);
242+
setCounter(counter + 1);
243+
}
244+
}
245+
246+
// Reset Race Values
247+
function handleRestart() {
248+
setStartTime(null);
249+
setEndTime(null);
250+
setInput("");
251+
setLine(0);
252+
setCounter(0);
253+
setErrorTotal(0);
254+
setErrors([]);
255+
}
256+
257+
return (
258+
<div
259+
className="w-3/4 lg:p-8 p-4 bg-accent rounded-md relative"
260+
onClick={focusOnLoad}
261+
role="none" // eslint fix - will remove the semantic meaning of an element while still exposing it to assistive technology
262+
>
263+
<RaceTracker
264+
codeLength={code.length}
265+
inputLength={input.length}
266+
user={user}
267+
/>
268+
<div className="mb-2 md:mb-4">
269+
<Heading
270+
title="Type this code"
271+
description="Start typing to get racing"
272+
/>
273+
</div>
274+
<Code code={code} errors={errors} userInput={input} />
275+
<input
276+
type="text"
277+
// value={input}
278+
defaultValue={input}
279+
ref={inputElement}
280+
onKeyDown={handleKeyboardEvent}
281+
disabled={endTime !== null}
282+
className="w-full h-full absolute p-8 inset-y-0 left-0 -z-40 focus:outline outline-blue-500 rounded-md"
283+
onPaste={(e) => e.preventDefault()}
284+
/>
285+
<TooltipProvider>
286+
<Tooltip>
287+
<TooltipTrigger asChild>
288+
<Button onClick={handleRestart}>Restart</Button>
289+
</TooltipTrigger>
290+
<TooltipContent>
291+
<p>Press Esc to reset</p>
292+
</TooltipContent>
293+
</Tooltip>
294+
</TooltipProvider>
295+
</div>
296+
);
297+
}

src/app/race/race-position-tracker.tsx renamed to src/app/race/RaceTracker.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ import {
55
import Image from "next/image";
66
import type { User } from "next-auth";
77

8-
interface RacePositionProps {
8+
interface RaceTrackerProps {
9+
codeLength: number;
910
inputLength: number;
10-
actualSnippetLength: number;
1111
user?: User;
1212
}
1313

1414
const GOAL_COMPLETED = 100;
1515

16-
export default function RacePositionTracker({
16+
export default function RaceTracker({
17+
codeLength,
1718
inputLength,
18-
actualSnippetLength,
1919
user,
20-
}: RacePositionProps) {
21-
const progress = (inputLength / actualSnippetLength) * 100;
20+
}: RaceTrackerProps) {
21+
const progress = (inputLength / codeLength) * 100;
2222

2323
return (
24-
<div className="relative flex items-center mb-5">
24+
<div className="relative mb-5 flex items-center">
2525
<ProgressBar>
2626
<ProgressIndicator progress={progress}>
2727
{progress !== GOAL_COMPLETED && (

src/app/race/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React, { Dispatch, SetStateAction, useState } from "react";
2+
import React, { SetStateAction, useState } from "react";
33
import { Button, buttonVariants } from "@/components/ui/button";
44
import { Card, CardContent, CardHeader } from "@/components/ui/card";
55
import { Users, User } from "lucide-react";

0 commit comments

Comments
 (0)