- )
+ );
}
-export default App
+export default App;
diff --git a/client/src/assets/logo.png b/client/src/assets/logo.png
new file mode 100644
index 0000000..009c3ca
Binary files /dev/null and b/client/src/assets/logo.png differ
diff --git a/client/src/assets/sounds/dry-fart.mp3 b/client/src/assets/sounds/dry-fart.mp3
new file mode 100644
index 0000000..8092a40
Binary files /dev/null and b/client/src/assets/sounds/dry-fart.mp3 differ
diff --git a/client/src/assets/sounds/oof.mp3 b/client/src/assets/sounds/oof.mp3
new file mode 100644
index 0000000..5bab911
Binary files /dev/null and b/client/src/assets/sounds/oof.mp3 differ
diff --git a/client/src/assets/sounds/osu-hit-sound.mp3 b/client/src/assets/sounds/osu-hit-sound.mp3
new file mode 100644
index 0000000..e56f31d
Binary files /dev/null and b/client/src/assets/sounds/osu-hit-sound.mp3 differ
diff --git a/client/src/assets/sounds/slam.mp3 b/client/src/assets/sounds/slam.mp3
new file mode 100644
index 0000000..81bd3cb
Binary files /dev/null and b/client/src/assets/sounds/slam.mp3 differ
diff --git a/client/src/assets/sounds/vine-boom-sound-effect-full.mp3 b/client/src/assets/sounds/vine-boom-sound-effect-full.mp3
new file mode 100644
index 0000000..e832b67
Binary files /dev/null and b/client/src/assets/sounds/vine-boom-sound-effect-full.mp3 differ
diff --git a/client/src/assets/sounds/wii-keyboard-click-3.mp3 b/client/src/assets/sounds/wii-keyboard-click-3.mp3
new file mode 100644
index 0000000..5e98d18
Binary files /dev/null and b/client/src/assets/sounds/wii-keyboard-click-3.mp3 differ
diff --git a/client/src/components/About.jsx b/client/src/components/About.jsx
index 024bf96..e511b5e 100644
--- a/client/src/components/About.jsx
+++ b/client/src/components/About.jsx
@@ -1,80 +1,86 @@
import React, { useEffect, useState, useRef } from "react";
import { useNavigate, useLocation } from "react-router-dom";
-import {FontAwesomeIcon} from "@fontawesome/react-fontawesome"
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
+import 'bootstrap/dist/css/bootstrap.min.css';
+import reactLogo from '../assets/logo.png'
const About = () => {
return (
-
-
- {/* This will hold the logo and the Title */}
-
-
-
Zoom Zoom Type
+
+
+ { /* About page header*/ }
+
+
About Us
+
ZoomZoomType
+
+
-
- {/* Add the other items to the bar */}
-
-
-
-
Average WPM:
-
+ { /* About our group, how the idea started etc. */ }
+
+
+ {/* Image on the left */}
+
+
+
+
+
-
- {/* Settings and Profile Icons on the far right */}
-
-
+
+ {/* Text on the right — restored to original column with padding */}
+
+
+
+ ZoomZoomType is an interactive web-based typing game
+ designed to challenge users to improve their typing
+ speed and accuracy. Players can race against the
+ clock — or against each other for the top spot — by typing given
+ passages as quickly and precisely as possible.
+ Built for both casual users looking to sharpen their
+ skills and competitive players aiming for the top of
+ the leaderboard, ZoomZoomType brings a fun, dynamic,
+ and fast-paced experience to the world of online typing
+ games.
+
+
+
+
-
-
-
+
+
-
-
-
About Us
-
ZoomZoomType
-
+ { /* Another text section? */ }
+
+
+
+
+ ZoomZoomType tracks detailed performance statistics like words
+ per minute (WPM) and accuracy, giving players a way to monitor
+ their progress and climb leaderboards. Built with a focus on
+ fast-paced gameplay and
+ community-driven competition. Whether your aiming for a
+ personal best or battling it out for the top spot, it
+ offers an engaging, competitive, and motivating environment.
-
-
- Text section regarding what we do as a group,
- how we started the idea, yadda yadda yadda
- Lorem ipsum dolor sit amet, consectetur adipiscing elit,
- sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
- Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
- reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
- Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum.
-
-
-
-
-
- Picture detailing service offered
-
-
-
-
- Another text section regarding what we do as a group,
- how we started the idea, yadda yadda yadda
- Lorem ipsum dolor sit amet, consectetur adipiscing elit,
- sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
- Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
- reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
- Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum.
+ ZoomZoomType was created as a collaborative group project for
+ our Human Interfce Computing course. Our team worked closely
+ together throught the planning, design, and development phases,
+ applying best practices in full-stack development, version control,
+ and agile methodology.
-
+
-
)
}
-export default About;
\ No newline at end of file
+export default About;
diff --git a/client/src/components/ClassicGame.jsx b/client/src/components/ClassicGame.jsx
new file mode 100644
index 0000000..47e38a6
--- /dev/null
+++ b/client/src/components/ClassicGame.jsx
@@ -0,0 +1,399 @@
+import React, { useState, useEffect, useRef, useMemo } from "react";
+import { useNavigate } from "react-router-dom";
+import { generate, count } from "random-words";
+import axios from "axios";
+
+const NUMB_OF_WORD = 170;
+const SECONDS = 60;
+
+const ClassicGame = ({ cookie, theme }) => {
+ //used placeholder for future gamemodes
+ const [mode, setMode] = useState("Random Lowercase Words");
+ //place holder for ability to set time
+ const [time, setTime] = useState(60);
+ //future calculated wpm
+ const [wpm, setWpm] = useState(0);
+ //const [targetText, setTargetText] = useState("Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos");
+ const [words, setWords] = useState([]);
+ //text user types
+ const [typedWord, setTypedWord] = useState("");
+ //Game status
+ const [gameStatus, setGameStatus] = useState(false);
+ //IsTextGenerated?
+ const [textGenerated, setTextGenerated] = useState(true);
+ //iterator for tracking which word user is at in word array
+ const [it, setIt] = useState(0);
+ //correct characters typed
+ const [correctChars, setCorrectChars] = useState(0);
+ //defining input ref so when game starts, you can automatically type on keyboard without clicking it
+ const inputRef = useRef(null);
+ //current word iterator
+ const [currIt, setCurrIt] = useState(0);
+ //state for words per line so we can have dynamic screen sizes
+ const [wordsPerLine, setWordsPerLine] = useState(12);
+ //state for how many characters per line can exist for the purposes of resizing screen dynamically
+ const [charactersPerLine, setCharactersPerLine] = useState(0);
+ //reference for container containing typed words for the purposes of reszizing
+ const typingContainerRef = useRef(null);
+ //char reference used for calculating character width which is using for the purposes of resizing typing container
+ const charRef = useRef(null);
+ //state fortracking final wpm, never gets set to zero
+ const finalWpmRef = useRef(0);
+
+ //Auxiliary functions to help with game
+ function generateWords() {
+ //generate list of words
+ let tempWords = generate(NUMB_OF_WORD);
+ //add a space to every word except for the last one
+ for (let i = 0; i < tempWords.length - 1; i++) {
+ tempWords[i] = tempWords[i] + " ";
+ }
+ setWords(tempWords);
+ }
+
+ //got this from chatgpt
+ function formatTime(time) {
+ const minutes = Math.floor(time / 60);
+ const seconds = time % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+ }
+
+ const navigate = useNavigate();
+ function handleClick(page) {
+ navigate(page);
+ }
+
+ //handles starting the game from the start game button
+ function start() {
+ setGameStatus(true);
+ }
+
+ async function postGameData(finalWpm) {
+ console.log("posting game data");
+ //get user information
+ let userData = cookie.usr;
+ //store data in object
+ const data = {
+ userID: userData.userID,
+ wpm: finalWpm,
+ time: 60,
+ mode: 1,
+ };
+ //post
+ try {
+ await axios.post("http://localhost:3000/api/postNewGame", data);
+ } catch (e) {
+ console.log("Error posting game data");
+ }
+ }
+
+ //checks if typed input matches current word
+ function handleTypedInput(event) {
+ //grab typed input
+ let typedPhrase = event.target.value;
+
+ //add character to typed word
+ setTypedWord(typedPhrase);
+ if (typedPhrase === words[currIt]) {
+ //if game is running, allow characters to be recorded
+ if (gameStatus === true) {
+ //get correct characters typed so far
+ let charsTyped = correctChars;
+ //add current typedPhrase length to charsTyped
+ charsTyped += typedPhrase.length;
+ //set chars typed. This is used to calculate words per minute
+ setCorrectChars(charsTyped);
+ }
+ //increment the word
+ setCurrIt(currIt + 1);
+ //set input word to nothing
+ setTypedWord("");
+ }
+ }
+
+ //calculate words per minute which is characters per second divided by 5
+ function calWPM(numChars, time) {
+ //convert characters to words
+ let words_ = numChars / 5;
+ //calculate wpm if
+ if (SECONDS - time != 0) {
+ //calculate seconds elapsed
+ let secondsElapsed = SECONDS - time;
+ //convert secondsElapsed to minutes elapsed
+ let minutesElapsed = secondsElapsed / 60;
+ //return wpm which is words/minutes
+ return words_ / minutesElapsed;
+ }
+ //return 0 if no time has passed
+ return 0;
+ }
+
+ //useEffect hooks for game logic
+ //This hook generates initial text when page first loads
+ useEffect(() => {
+ generateWords();
+ }, []);
+
+ //This hook is responsible for generating text when the game starts and resetting it
+ useEffect(() => {
+ //checks if gameStatus is true and runs the text generation if it is true
+ if (gameStatus === true) {
+ //set text counter to 0
+ setIt(0);
+ //set currIt counter to 0
+ setCurrIt(0);
+ //set typed word to nothing
+ setTypedWord("");
+ //if text is not generated, generate the text
+ if (textGenerated === false) {
+ generateWords();
+ setTextGenerated(true);
+ }
+ //set wpm
+ setWpm(0);
+ //set counted characters
+ setCorrectChars(0);
+ }
+ //auto focus on input bar so user doesn't have to manually click it after game starts
+ if (gameStatus === true && inputRef.current) {
+ inputRef.current.focus();
+ }
+
+ //hook runs when game Status updates
+ }, [gameStatus]);
+
+ //home responsible for managing the timer --will merge with first hook
+ //will also post scores when game ends
+ useEffect(() => {
+ if (gameStatus === true) {
+ //set time for timer to value of global variable SECONDS
+ let tempTime = SECONDS;
+ //set time to temp time before timer runs
+ setTime(tempTime);
+ //set timer that updates code roughly every second
+ const timer = setInterval(() => {
+ //decrement timer
+ tempTime = tempTime - 1;
+ //when timer hits zero, game is over
+ if (tempTime <= 0) {
+ //get final wpm
+ //disable the timer
+ clearInterval(timer);
+ //set gameStatus to false
+ setGameStatus(false);
+ //set textGenerated to false so new text can generate
+ setTextGenerated(false);
+ //get wpm
+ const finalWpm_ = finalWpmRef.current;
+ //check if user is logged in
+ if (cookie.usr) {
+ postGameData(finalWpm_).then(() => {});
+ }
+ }
+ //set time to new time
+ setTime(tempTime);
+ }, 1000);
+ }
+ }, [gameStatus]);
+
+ //useEffect loop for updating words per minute
+ useEffect(() => {
+ let tmpCorrectChars = correctChars;
+ let tmpWpm = calWPM(tmpCorrectChars, time);
+ if (tmpWpm != 0) {
+ finalWpmRef.current = Math.round(tmpWpm);
+ }
+ setWpm(Math.round(tmpWpm));
+ //useeffect function runs when correctChars changes
+ }, [correctChars]);
+
+ const handleResize = () => {
+ if (!typingContainerRef.current || !charRef.current) {
+ console.log("One or both refs are null");
+ return;
+ }
+
+ //get container padding
+ const containerElement = typingContainerRef.current;
+ const containerStyles = window.getComputedStyle(containerElement);
+ const containerPadding = parseFloat(containerStyles.paddingLeft) + parseFloat(containerStyles.paddingRight);
+
+ //get the container width
+ const containerWidth = typingContainerRef.current.offsetWidth - containerPadding;
+
+ //get the character width
+ const characterWidth = charRef.current.offsetWidth;
+ //calculate how many characters can fit within container
+ const charAmount = Math.floor(containerWidth / characterWidth);
+ //set characters per line
+ setCharactersPerLine(charAmount);
+ };
+
+ //use effect for handling screensize
+ useEffect(() => {
+ handleResize();
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ useEffect(() => {
+ handleResize(); // Run on initial render
+ }, []);
+
+ useEffect(() => {
+ if (gameStatus) {
+ setTimeout(() => {
+ handleResize(); // Run after the game starts
+ }, 0);
+ }
+ }, [gameStatus]);
+
+ //function for rendering the text, will be same across all games
+ const renderGame = useMemo(() => {
+ if (charactersPerLine === 0) {
+ return [];
+ }
+ //used to track indexes of all characters from 0 - n characters
+ let charIndex = 0;
+ //tracks the starting position of the current word we are typing
+ let currWordIndex = 0;
+ //set curr word index - must happen only once
+ let setCurrWordIndex = false;
+ //flag to mark all words typed after incorrect character red
+ let incorrectCharFound = false;
+ //current line
+ let line = [];
+ //set of lines
+ let lines = [];
+
+ //grab characters per line
+ let charsPerLine = charactersPerLine;
+ let charsPerLineSoFar = 0;
+ let wordsOnCurrentLine = 0;
+ let visibleWords = words.slice(it, it + 85);
+ //unique keys for letters in lines. Coutns number of characters total
+ let k = 0;
+
+ //set it forward if needed
+ if (currIt >= it + wordsPerLine) {
+ setIt(currIt);
+ }
+
+ //iterate through all words from iterator marker onwards
+ let wordIt = 0;
+ while (wordIt < visibleWords.length && lines.length < 4) {
+ if (charsPerLineSoFar + visibleWords[wordIt].length <= charsPerLine) {
+ //add word length to charsPerLineSoFar
+ charsPerLineSoFar += visibleWords[wordIt].length;
+ //add every letter to line through rendering logic
+ for (let letterIt = 0; letterIt < visibleWords[wordIt].length; letterIt++) {
+ //rendering logic
+ //default rendering styles
+ let styling = {};
+ let charSpan = ;
+ //if word has already been typed
+ if (wordIt + it < currIt) {
+ styling = { color: "white" };
+ charSpan = (
+
+ {visibleWords[wordIt][letterIt]}
+
+ );
+ } else {
+ if (wordIt + it === currIt) {
+ if (setCurrWordIndex === false) {
+ currWordIndex = k;
+ setCurrWordIndex = true;
+ }
+ }
+
+ //check if character has been typed
+ if (k - currWordIndex < typedWord.length) {
+ //if it has been typed, check if correct, if correct make it white
+ if (visibleWords[wordIt][letterIt] === typedWord[letterIt] && incorrectCharFound === false) {
+ styling = { color: "white", position: "relative" };
+ } else {
+ styling = { color: "red", position: "relative" };
+ incorrectCharFound = true;
+ }
+ } else {
+ styling = { color: "grey", position: "relative" };
+ }
+
+ //check whether to print cursor or not
+ if (k - currWordIndex === typedWord.length) {
+ charSpan = (
+
+ {visibleWords[wordIt][letterIt]}
+
+
+ );
+ } else {
+ charSpan = (
+
+ {visibleWords[wordIt][letterIt]}
+
+ );
+ }
+ }
+ line.push(charSpan);
+ //increment unique key
+ k++;
+ }
+ //increment wordIt;
+ wordIt++;
+ //increment words on current line
+ wordsOnCurrentLine++;
+ } else {
+ //push the line of words into lines
+ lines.push(
{line}
);
+ //reset charsperlinesofar and line and wordsOnCurrentline
+ if (lines.length == 1) {
+ setWordsPerLine(wordsOnCurrentLine);
+ }
+ charsPerLineSoFar = 0;
+ line = [];
+ wordsOnCurrentLine = 0;
+ }
+ }
+
+ return lines;
+ }, [words, it, typedWord, wordsPerLine, charactersPerLine]);
+
+ return (
+
+
+
Mode: {mode}
+
Time: {formatTime(time)}
+
wpm: {wpm}
+
+ {/*Conditionally render game is game is running or not*/}
+ {gameStatus && (
+
+ )
+}
+
+export default ProfilePage;
\ No newline at end of file
diff --git a/client/src/components/QuoteGame.jsx b/client/src/components/QuoteGame.jsx
new file mode 100644
index 0000000..5e61a26
--- /dev/null
+++ b/client/src/components/QuoteGame.jsx
@@ -0,0 +1,454 @@
+import React, { useState, useEffect, useRef, useMemo } from "react";
+import { useNavigate } from "react-router-dom";
+import { generate, count } from "random-words";
+import axios from "axios";
+
+const NUMB_OF_WORD = 170;
+const SECONDS = 60;
+
+const QuoteGame = ({ cookie }) => {
+ //used placeholder for future gamemodes
+ const [mode, setMode] = useState("Random Quote");
+ //place holder for ability to set time
+ const [time, setTime] = useState(0);
+ //future calculated wpm
+ const [wpm, setWpm] = useState(0);
+ //const [targetText, setTargetText] = useState("Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos");
+ const [words, setWords] = useState([]);
+ //text user types
+ const [typedWord, setTypedWord] = useState("");
+ //Game status
+ const [gameStatus, setGameStatus] = useState(false);
+ //IsTextGenerated?
+ const [textGenerated, setTextGenerated] = useState(true);
+ //iterator for tracking which word user is at in word array
+ const [it, setIt] = useState(0);
+ //correct characters typed
+ const [correctChars, setCorrectChars] = useState(0);
+ //defining input ref so when game starts, you can automatically type on keyboard without clicking it
+ const inputRef = useRef(null);
+ //current word iterator
+ const [currIt, setCurrIt] = useState(0);
+ //state for words per line so we can have dynamic screen sizes
+ const [wordsPerLine, setWordsPerLine] = useState(12);
+ //state for how many characters per line can exist for the purposes of resizing screen dynamically
+ const [charactersPerLine, setCharactersPerLine] = useState(0);
+ //reference for container containing typed words for the purposes of reszizing
+ const typingContainerRef = useRef(null);
+ //char reference used for calculating character width which is using for the purposes of resizing typing container
+ const charRef = useRef(null);
+ //state fortracking final wpm, never gets set to zero
+ const finalWpmRef = useRef(0);
+ //state for tracking when to stop timer
+ const endTime = useRef(false);
+ //state for tracking quoteID
+ const [quoteID, setQuoteID] = useState("none");
+
+ //Auxiliary functions to help with game
+ async function getRandomQuote() {
+ try {
+ //get the quote from the server
+ const response = await axios.get("http://localhost:3000/api/randomQuote");
+ //turn it into text
+ const quote = response.data.quote;
+ setQuoteID(response.data.quoteID);
+ //parse the words
+ const parsedQuote = quote.split(" ").map((word, index, array) => {
+ return index < array.length - 1 ? word + " " : word;
+ });
+ setWords(parsedQuote);
+ console.log(parsedQuote);
+ } catch (error) {
+ console.error("Error fetching random quote from server");
+ //turn it into text
+ const quote = "The Quote does not exist but it will exist soon.";
+ //parse the words
+ const parsedQuote = quote.split(" ").map((word, index, array) => {
+ return index < array.length - 1 ? word + " " : word;
+ });
+ setWords(parsedQuote);
+ setQuoteID(response.data.quoteID);
+ }
+ }
+
+ //got this from chatgpt
+ function formatTime(time) {
+ const minutes = Math.floor(time / 60);
+ const seconds = time % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+ }
+
+ const navigate = useNavigate();
+ function handleClick(page) {
+ navigate(page);
+ }
+
+ //handles starting the game from the start game button
+ function start() {
+ setGameStatus(true);
+ endTime.current = false;
+ }
+
+ async function postGameData(finalWpm) {
+ console.log("posting game data");
+ //get user information
+ let userData = cookie.usr;
+ //store data in object
+ //grab time
+ let timeTaken = time;
+ const data = {
+ userID: userData.userID,
+ wpm: finalWpm,
+ time: timeTaken,
+ mode: 3,
+ quoteID: quoteID,
+ };
+
+ //post
+ try {
+ await axios.post("http://localhost:3000/api/postNewGame", data);
+ } catch (e) {
+ console.log("Error posting game data");
+ }
+ }
+
+ //checks if typed input matches current word
+ function handleTypedInput(event) {
+ //grab typed input
+ let typedPhrase = event.target.value;
+
+ //add character to typed word
+ setTypedWord(typedPhrase);
+ if (typedPhrase === words[currIt]) {
+ //if game is running, allow characters to be recorded
+ if (gameStatus === true) {
+ //get correct characters typed so far
+ let charsTyped = correctChars;
+ //add current typedPhrase length to charsTyped
+ charsTyped += typedPhrase.length;
+ //set chars typed. This is used to calculate words per minute
+ setCorrectChars(charsTyped);
+ }
+ //increment the word
+ setCurrIt(currIt + 1);
+ //set input word to nothing
+ setTypedWord("");
+
+ //check if user has completed all words
+ if (currIt + 1 === words.length) {
+ //end game
+ endGame();
+ }
+ }
+ }
+
+ function endGame() {
+ setGameStatus(false);
+ endTime.current = true;
+ const finalWpm_ = finalWpmRef.current;
+
+ if (cookie.usr) {
+ postGameData(finalWpm_).then(() => {
+ console.log("Game data posted successfully.");
+ });
+ }
+ }
+
+ //calculate words per minute which is characters per second divided by 5
+ function calWPM(numChars, time) {
+ //convert characters to words
+ let words_ = numChars / 5;
+ //calculate wpm if
+ if (time != 0) {
+ //calculate seconds elapsed
+ let secondsElapsed = time;
+ //convert secondsElapsed to minutes elapsed
+ let minutesElapsed = secondsElapsed / 60;
+ //return wpm which is words/minutes
+ return words_ / minutesElapsed;
+ }
+ //return 0 if no time has passed
+ return 0;
+ }
+
+ //useEffect hooks for game logic
+ //This hook generates initial text when page first loads
+ useEffect(() => {
+ getRandomQuote();
+ }, []);
+
+ //This hook is responsible for generating text when the game starts and resetting it
+ useEffect(() => {
+ //checks if gameStatus is true and runs the text generation if it is true
+ if (gameStatus === true) {
+ //set text counter to 0
+ setIt(0);
+ //set currIt counter to 0
+ setCurrIt(0);
+ //set typed word to nothing
+ setTypedWord("");
+ //if text is not generated, generate the text
+ if (textGenerated === false) {
+ getRandomQuote();
+ setTextGenerated(true);
+ }
+ //set wpm
+ setWpm(0);
+ //set counted characters
+ setCorrectChars(0);
+ }
+ //auto focus on input bar so user doesn't have to manually click it after game starts
+ if (gameStatus === true && inputRef.current) {
+ inputRef.current.focus();
+ }
+
+ //hook runs when game Status updates
+ }, [gameStatus]);
+
+ //home responsible for managing the timer --will merge with first hook
+ //will also post scores when game ends
+ useEffect(() => {
+ if (gameStatus === true) {
+ //set time for timer to value of global variable SECONDS
+ let tempTime = 0;
+ //set time to temp time before timer runs
+ setTime(tempTime);
+ //set timer that updates code roughly every second
+ const timer = setInterval(() => {
+ //increment timer
+ tempTime = tempTime + 1;
+ //when timer hits zero, game is over
+ if (endTime.current === true) {
+ //disable the timer
+ clearInterval(timer);
+
+ // //set gameStatus to false
+ // setGameStatus(false);
+ // //set textGenerated to false so new text can generate
+ // setTextGenerated(false);
+ // //get wpm
+ // const finalWpm_ = finalWpmRef.current;
+ // //check if user is logged in
+ // if(cookie.usr){
+ // postGameData(finalWpm_).then(() => {
+ // });
+ // }
+ }
+ //set time to new time
+ setTime(tempTime);
+ }, 1000);
+ }
+ }, [gameStatus]);
+
+ //useEffect loop for updating words per minute
+ useEffect(() => {
+ let tmpCorrectChars = correctChars;
+ let tmpWpm = calWPM(tmpCorrectChars, time);
+ if (tmpWpm != 0) {
+ finalWpmRef.current = Math.round(tmpWpm);
+ }
+ setWpm(Math.round(tmpWpm));
+ //useeffect function runs when correctChars changes
+ }, [correctChars]);
+
+ const handleResize = () => {
+ if (!typingContainerRef.current || !charRef.current) {
+ console.log("One or both refs are null");
+ return;
+ }
+
+ //get container padding
+ const containerElement = typingContainerRef.current;
+ const containerStyles = window.getComputedStyle(containerElement);
+ const containerPadding = parseFloat(containerStyles.paddingLeft) + parseFloat(containerStyles.paddingRight);
+
+ //get the container width
+ const containerWidth = typingContainerRef.current.offsetWidth - containerPadding;
+
+ //get the character width
+ const characterWidth = charRef.current.offsetWidth;
+ //calculate how many characters can fit within container
+ const charAmount = Math.floor(containerWidth / characterWidth);
+ //set characters per line
+ setCharactersPerLine(charAmount);
+ };
+
+ //use effect for handling screensize
+ useEffect(() => {
+ handleResize();
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ useEffect(() => {
+ handleResize(); // Run on initial render
+ }, []);
+
+ useEffect(() => {
+ if (gameStatus) {
+ setTimeout(() => {
+ handleResize(); // Run after the game starts
+ }, 0);
+ }
+ }, [gameStatus]);
+
+ //function for rendering the text, will be same across all games
+ const renderGame = useMemo(() => {
+ if (charactersPerLine === 0) {
+ return [];
+ }
+ //used to track indexes of all characters from 0 - n characters
+ let charIndex = 0;
+ //tracks the starting position of the current word we are typing
+ let currWordIndex = 0;
+ //set curr word index - must happen only once
+ let setCurrWordIndex = false;
+ //flag to mark all words typed after incorrect character red
+ let incorrectCharFound = false;
+ //current line
+ let line = [];
+ //set of lines
+ let lines = [];
+
+ //grab characters per line
+ let charsPerLine = charactersPerLine;
+ let charsPerLineSoFar = 0;
+ let wordsOnCurrentLine = 0;
+ let visibleWords = words.slice(it, it + 85);
+ //unique keys for letters in lines. Coutns number of characters total
+ let k = 0;
+
+ //set it forward if needed
+ if (currIt >= it + wordsPerLine) {
+ setIt(currIt);
+ }
+
+ //iterate through all words from iterator marker onwards
+ let wordIt = 0;
+ while (wordIt < visibleWords.length && lines.length < 4) {
+ if (charsPerLineSoFar + visibleWords[wordIt].length <= charsPerLine) {
+ //add word length to charsPerLineSoFar
+ charsPerLineSoFar += visibleWords[wordIt].length;
+ //add every letter to line through rendering logic
+ for (let letterIt = 0; letterIt < visibleWords[wordIt].length; letterIt++) {
+ //rendering logic
+ //default rendering styles
+ let styling = {};
+ let charSpan = ;
+ //if word has already been typed
+ if (wordIt + it < currIt) {
+ styling = { color: "white" };
+ charSpan = (
+
+ {visibleWords[wordIt][letterIt]}
+
+ );
+ } else {
+ if (wordIt + it === currIt) {
+ if (setCurrWordIndex === false) {
+ currWordIndex = k;
+ setCurrWordIndex = true;
+ }
+ }
+
+ //check if character has been typed
+ if (k - currWordIndex < typedWord.length) {
+ //if it has been typed, check if correct, if correct make it white
+ if (visibleWords[wordIt][letterIt] === typedWord[letterIt] && incorrectCharFound === false) {
+ styling = { color: "white", position: "relative" };
+ } else {
+ styling = { color: "red", position: "relative" };
+ incorrectCharFound = true;
+ }
+ } else {
+ styling = { color: "grey", position: "relative" };
+ }
+
+ //check whether to print cursor or not
+ if (k - currWordIndex === typedWord.length) {
+ charSpan = (
+
+ {visibleWords[wordIt][letterIt]}
+
+
+ );
+ } else {
+ charSpan = (
+
+ {visibleWords[wordIt][letterIt]}
+
+ );
+ }
+ }
+ line.push(charSpan);
+ //increment unique key
+ k++;
+ }
+ //increment wordIt;
+ wordIt++;
+ //increment words on current line
+ wordsOnCurrentLine++;
+ } else {
+ //push the line of words into lines
+ lines.push(
{line}
);
+ //reset charsperlinesofar and line and wordsOnCurrentline
+
+ if (lines.length === 1) {
+ setWordsPerLine(wordsOnCurrentLine);
+ }
+ charsPerLineSoFar = 0;
+ line = [];
+ wordsOnCurrentLine = 0;
+ }
+ }
+
+ //Push remaining characters onto lines
+ if (line.length > 0) {
+ lines.push(
{line}
);
+
+ //set wordsPerLine if this is the first line
+ if (lines.length === 1) {
+ setWordsPerLine(wordsOnCurrentLine);
+ }
+ }
+
+ return lines;
+ }, [words, it, typedWord, wordsPerLine, charactersPerLine]);
+
+ return (
+
+
+
Mode: {mode}
+
Time: {formatTime(time)}
+
wpm: {wpm}
+
+ {/*Conditionally render game is game is running or not*/}
+ {gameStatus && (
+
Either the username or password is incorrect or an account does not exist under the provided email, please try again
+ );
+ } else {
+ return null;
+ }
+ }
+
+ //Handles the login button click
+ const handleLogin = async (e) => {
+ //Prevent the default action of submission, will do with axios
+ e.preventDefault();
+
+ //Clear the error message state since trying to log in again
+ setLoginError(false);
+
+ console.log("handling the login!!!");
+ console.log(`formValue.email: ${formValue.email}`);
+ console.log(`formValue.password: ${formValue.password}`);
+
+ //Now take the information and submit it to the backend with axios
+ let response = null;
+
+ try {
+ response = await axios.post("http://localhost:3000/api/login", formValue);
+ } catch {
+ //Now make response be an {} with status 401
+ //Force it to be a fail HTTP status code
+ response = { status: 401 };
+ }
+
+ //Now check in on the response code
+ if (response.status == 200) {
+ //Good
+ console.log("Login successful");
+
+ const email = formValue.email;
+ console.log(`formvalue.email: ${formValue.email}`);
+
+ console.log(response.data.userName);
+
+ //Make the JSON to put into the cookie, so usrID, usrName
+ //The login query already sends back all three to use!
+ const cookieJSON = {
+ userUsername: response.data.userName,
+ userID: response.data.userID,
+ userEmail: response.data.userEmail,
+ };
+
+ console.log(cookieJSON);
+
+ //Grab the avg WPM
+ const userAvgWPM = await axios.get("http://localhost:3000/api/getAvgWPM", { params: { email } });
+
+ console.log(userAvgWPM);
+
+ //Set the WPM global state
+ updateAvgWPM(userAvgWPM.data.avgWPM);
+
+ console.log(userAvgWPM.data.avgWPM);
+
+ console.log(`userAvgWPM: {userAvgWPM}`);
+
+ //make the cookie
+ setLoginCookie("usr", JSON.stringify(cookieJSON), { path: "/" });
+
+ //Redirect to the homepage
+ return ;
+ } else {
+ //clear the fields
+
+ //Make the error message show by modifying the state
+ setLoginError(true);
+ }
+ };
+
+ //
+ // Checking if the user is logged in, if so, redirect to profile
+ //
+ const loginCookie = cookie.usr ? JSON.stringify(cookie.usr) : null;
+
+ if (loginCookie == null) {
+ return (
+
+
+
+
+
+
+
+ );
+ } else {
+ //User is already logged in, now redirect to the homepage
+ return ;
+ }
+};
+
+export default LogInPage;
diff --git a/client/src/main.jsx b/client/src/main.jsx
index b877058..2747051 100644
--- a/client/src/main.jsx
+++ b/client/src/main.jsx
@@ -1,11 +1,13 @@
-import React from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.jsx'
-import { BrowserRouter } from "react-router-dom"
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App.jsx";
+import { BrowserRouter } from "react-router-dom";
+import { CookiesProvider } from "react-cookie";
-
-createRoot(document.getElementById('root')).render(
-
-
-
-)
+createRoot(document.getElementById("root")).render(
+
+
+
+
+
+);
diff --git a/client/vite.config.js b/client/vite.config.js
index 8b0f57b..bce19db 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,7 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [react()],
-})
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ hmr: true,
+})
diff --git a/server/DB/db.js b/server/DB/db.js
new file mode 100644
index 0000000..39b4500
--- /dev/null
+++ b/server/DB/db.js
@@ -0,0 +1,24 @@
+//This will run queries from the db and return the results
+const sqlite = require('better-sqlite3');
+const path = require('path');
+const db = new sqlite(path.resolve(__dirname, 'zoomzoomtypeDB.db'), {fileMustExist: true});
+
+function query(sql, params=[]) {
+ return db.prepare(sql).all(params);
+}
+
+function deleteQuery(sql, params){
+ db.prepare(sql).run(params);
+
+}
+
+//Done for all queries that do not return any data ONLY!!!
+function noReturnQuery(sql, params) {
+ db.prepare(sql).run(params);
+}
+
+module.exports = {
+ query,
+ deleteQuery,
+ noReturnQuery
+}
\ No newline at end of file
diff --git a/server/DB/queries.js b/server/DB/queries.js
new file mode 100644
index 0000000..69da565
--- /dev/null
+++ b/server/DB/queries.js
@@ -0,0 +1,419 @@
+const db = require('./db');
+const {check, validationResult } = require('express-validator');
+
+
+//
+// READ/GET
+//
+
+//Assumed to already be sanitized by the backend before being sent to query
+function getPasswordByEmail(email) {
+ if (email == null) {
+ //No email was given, so return false
+ return null;
+ }
+
+ //There is an email, so query the database to check
+ result = db.query('SELECT userPassword FROM users WHERE userEmail == ?;', [email]);
+
+ //Return the first result (only result) and the userPassword value from the returned dictionary
+ pass = null;
+
+ try {
+ //Try to grab the password from the result
+ pass = result[0].userPassword;
+ } catch {
+ //No password was found, so set null
+ pass = null;
+ }
+
+ return pass;
+}
+
+
+function getUserIDByEmail(email) {
+ if (email == null) {
+ //No email was given, so return false
+ return null;
+ }
+
+ //There is an email, so query the database to check
+ result = db.query('SELECT userID FROM users WHERE userEmail == ?;', [email]);
+
+ //Return the first result (only result) and the userPassword value from the returned dictionary
+ id = null;
+
+ try {
+ //Try to grab the password from the result
+ id = result[0].userID;
+ } catch {
+ //No password was found, so set null
+ id = null;
+ }
+
+ return id;
+}
+
+
+function getUserNameByID(userID) {
+ //Check if userID is null
+ if (userID == null) {
+ return;
+ }
+
+
+ //Now execute the query
+ result = db.query("SELECT userUsername FROM users WHERE userID = ?;", [ userID ]);
+
+ usrName = null;
+
+ try{
+ usrName = result[0].userUsername;
+ }catch{
+ throw "getUserNameByID: Could not get the user's username from the SQL Database Query";
+ }
+
+ return usrName;
+
+}
+
+function getAvgWPMByUserID(userID) {
+ //check if the userID is null
+ if (userID == null) {
+ return;
+ }
+
+ //Now query the db based on the userID
+ result = db.query("SELECT wpmTotal/gamesPlayed as average FROM avgWPM WHERE userID = ?;", [userID]);
+
+ avg = null;
+
+ try{
+ avg = result[0].average;
+ }catch{
+ throw "getAvgWPMByUserID: Could not get average from the SQL Database Query";
+ }
+
+ return avg;
+
+}
+
+function getGamesPlayedAndWPMByUserID(userID) {
+
+ //check for null userID
+ if (userID == null) {
+ throw "getGamesPlayedAndWPMByUserID: userID can not be null";
+ }
+
+ //Good to go, now grab the users WPM and games played by their userID
+ result = db.query("SELECT wpmTotal, gamesPlayed FROM avgWPM WHERE userID = ?;", [userID]);
+
+ //Will hold the games played and the over all WPM
+ gamesPlayed = 0;
+ wpmTotal = 0;
+
+ console.log(result[0].gamesPlayed);
+ console.log(result[0]);
+
+ try {
+ gamesPlayed = result[0].gamesPlayed;
+ wpmTotal = result[0].wpmTotal;
+ } catch{
+ throw `getGamesPlayedAndWPMByUserID: could not either assign gamesPlayed or wpmTotal: ${e}`;
+ }
+
+ return result[0];
+}
+
+function getProfileDataByID(userID) {
+ //Ensure the userID is not null
+ if (userID == null) {
+ throw "getProfileDataByID: userID can not be null";
+ }
+
+ //Now that the userID is not null, the query can be executed
+ //THIS IS A LONG QUERY!!!!!!
+ let result = db.query(`SELECT
+ ranked.rank AS ranking,
+ ranked.wpm,
+ CASE
+ WHEN ranked.mode = 1 THEN 'Classic'
+ WHEN ranked.mode = 2 THEN 'Memorize'
+ WHEN ranked.mode = 3 THEN 'Quote'
+ WHEN ranked.mode = 4 THEN 'Look-Ahead'
+ END AS mode,
+ ranked.time,
+ ranked.dateOfGame,
+ ranked.accuracy
+ FROM (
+ SELECT
+ leaderBoardID,
+ userID,
+ wpm,
+ mode,
+ time,
+ quoteID,
+ dateOfGame,
+ accuracy,
+ RANK() OVER (ORDER BY wpm DESC) AS rank
+ FROM leaderBoard
+ ) AS ranked
+ LEFT JOIN quotes ON ranked.quoteID = quotes.quoteID
+ WHERE ranked.userID = ?;`, [userID]);
+
+ console.log(result);
+
+ return result;
+}
+
+function getLeaderBoardResults(gameMode) {
+ //Will hold the top 10 results for the mode
+ top10 = null;
+
+ //Default is classic (1)
+ let gameModeNumber = 1
+
+ //Sets the game mode number, this will be passed to the DB query
+ switch (gameMode) {
+ case "Classic":
+ gameModeNumber = 1;
+ break;
+ case "Memorize":
+ gameModeNumber = 2;
+ break;
+ case "Quotes":
+ gameModeNumber = 3;
+ break;
+ case "Look-Ahead":
+ gameModeNumber = 4;
+ break;
+ }
+
+ console.log(`getLeaderBoardResults: gameMode: ${gameMode} | gameModeNumber: ${gameModeNumber}`);
+
+ //Now we need to make the DB call based on the game mode, if the game mode is not 3
+ if (gameMode != 3) {
+ top10 = db.query("SELECT users.userUsername, wpm, time FROM leaderBoard JOIN users ON leaderBoard.userID = users.userID WHERE mode = ? ORDER BY wpm DESC LIMIT 10;", [gameModeNumber]);
+ }
+
+ return top10;
+}
+
+
+function getAllQuotes() {
+ //Will hold the results
+ let results = null;
+
+
+ //Now send query over
+ result = db.query("SELECT * FROM quotes;");
+
+ console.log(`getAllQuotes result: ${result[0]}`);
+ console.log(`getAllQuotes result: ${result[1]}`);
+
+ //Now return it
+ return result;
+}
+
+function getQuoteLeaderBoardByID(quoteID) {
+ console.log("Getting quote leaderboard by the quoteID");
+
+ //Now make the DB query to get the leaderboard based on quote ID
+ let result = null;
+
+ result = db.query("SELECT users.userUsername, wpm, TIME FROM leaderBoard JOIN users ON leaderBoard.userID = users.userID JOIN quotes ON leaderBoard.quoteID = quotes.quoteID WHERE mode = 3 AND leaderBoard.quoteID = ? ORDER by wpm DESC LIMIT 10;", [quoteID]);
+
+ return result;
+}
+
+function getRandomQuote() {
+ console.log("Getting random quote and quote ID");
+
+ let result = db.query("SELECT * FROM quotes ORDER BY RANDOM() LIMIT 1;", []);
+ result = result[0];
+
+ console.log(`getAllQuotes: result = ${result}`);
+
+ return result;
+}
+
+
+//
+// UPDATE
+//
+function updateAvgWPMByUserID(userID, gamesWPM){
+ if (userID == null){
+ console.log("updateAvgWPMByUserID: userID can not be null");
+ return null;
+ }
+
+ //variables to hold the new values to write to the database
+ newGamesPlayed = 0;
+ newResultWPM = 0;
+
+ //Get the current WPM and game count
+ result = db.query("SELECT wpmTotal, gamesPlayed from avgWPM WHERE userID = ?;", [userID]);
+
+
+ //Now assign the query results to each respective updated variable
+ try{
+ newResultWPM = result[0].wpmTotal;
+ } catch {
+ throw "updateAvgWPMByUserID: Could not get wmpTotal from the SQL Database Query";
+ }
+
+ console.log(`updateAvgWPMByUserID, userID: ${userID} | wpmTotal: ${newResultWPM}`);
+
+ try {
+ newGamesPlayed = result[0].gamesPlayed;
+ } catch {
+ throw "updateAvgWPMByUserID: Could not get gamesPlayed from the SQL Database Query";
+ }
+
+
+
+ //Update the values
+ newGamesPlayed++;
+ newResultWPM += gamesWPM;
+
+ //Now write an update query
+ db.noReturnQuery("UPDATE avgWPM SET wpmTotal = ?, gamesPlayed = ? WHERE userID = ?;", [newResultWPM, newGamesPlayed, userID]);
+}
+
+
+function updateUserNameByID(ID, newUserName) {
+ console.log("Updating username by ID with DB");
+
+
+ //Check for NULL ID
+ if (ID == null) {
+ throw "updateUserNameByID: ID can not be null";
+ } else if (newUserName == null) {
+ throw "updateUserNameByID: newUserName can not be null"
+ }
+
+ //Now make the update query
+ db.noReturnQuery("UPDATE users SET userUsername = ? WHERE userID = ?;", [newUserName, ID]);
+}
+
+function updateEmailByID(ID, newEmail) {
+ console.log("Updating email by ID with DB");
+
+ //Check for NULL id and email
+ if (ID == null) {
+ throw "updateEmailByID: ID can not be null";
+
+ } else if (newEmail == null) {
+ throw "updateEmailByID: newEmail can not be null";
+ }
+
+
+
+ //Make call to DB to update info
+ db.noReturnQuery("UPDATE users SET userEmail = ? WHERE userID = ?;", [newEmail, ID]);
+}
+
+function updatePasswordByID(ID, newPassword) {
+ console.log("Updating password by ID with DB");
+
+ //Check for NULL id and email
+ if (ID == null) {
+ throw "updatePasswordByID: ID can not be null";
+
+ } else if (newPassword == null) {
+ throw "updatePasswordByID: newPassword can not be null";
+ }
+
+
+
+ //Make call to DB to update info
+ db.noReturnQuery("UPDATE users SET userPassword = ? WHERE userID = ?;", [newPassword, ID]);
+}
+
+
+function addScoreToDatabase(userID, wpm, mode, time, quoteID=null) {
+ //Send this to the database, this is any mode
+ //BUT NOT QUOTE (3)
+ if (quoteID == null && mode != 3) {
+ db.noReturnQuery("INSERT INTO leaderBoard (userID, wpm, mode, time) VALUES (?, ?, ?, ?);", [userID, wpm, mode, time]);
+ } else {
+ //This is quoted insert!
+ db.noReturnQuery("INSERT INTO leaderBoard (userID, wpm, mode, time, quoteID) VALUES (?, ?, ?, ?, ?);", [userID, wpm, mode, time, quoteID]);
+ }
+}
+
+//
+// CREATE
+//
+
+function createUserAccount(userName, userEmail, password) {
+ //Password is already assumed to be confirmed to be the same
+
+ //Null checks
+ if (userName == null) {
+ throw "createUserAccount: userName can not be null";
+ } else if (userEmail == null) {
+ throw "createUserAccount: userEmail can not be null";
+ } else if (password == null) {
+ throw "createUserAccount: password can not be null";
+ }
+
+ //Create the user account
+ db.noReturnQuery("INSERT INTO users (userUsername , userEmail , userPassword) VALUES (?, ?, ?);", [userName, userEmail, password]);
+
+ //Holds the userID of the new account to make an entry into the avgWPM table
+ userID = null;
+
+ userIDResult = db.query("SELECT userID FROM users WHERE userEmail = ?", [userEmail]);
+
+ //Try to assign it to the userID variable
+ try {
+ userID = userIDResult[0].userID;
+ } catch {
+ throw "createUserAccount: Can not grab the userID of the newly created account from the database";
+ }
+
+ //Now that its assigned, create a new entry with 0's for wpmTotal and gamesPlayed to it for the user
+ db.noReturnQuery("INSERT INTO avgWPM (wpmTotal, userID, gamesPlayed) VALUES (?, ?, ?);", [0, userID, 0]);
+
+}
+
+//
+// DELETE
+//
+
+function deleteUserByID(ID) {
+
+ //Check for NULL ID
+ if (ID == null) {
+ throw "deleteUserByID: ID can not be null";
+ }
+
+ //Now good to execute the query
+ try {
+ db.deleteQuery("DELETE FROM users WHERE userID = ?;", [ID]);
+ } catch (e) {
+ console.log(`deleteUserByID: ${e}`);
+ }
+
+}
+
+module.exports = {
+ getPasswordByEmail,
+ getUserIDByEmail,
+ getAvgWPMByUserID,
+ updateAvgWPMByUserID,
+ getUserNameByID,
+ addScoreToDatabase,
+ getGamesPlayedAndWPMByUserID,
+ getLeaderBoardResults,
+ deleteUserByID,
+ createUserAccount,
+ updatePasswordByID,
+ updateEmailByID,
+ updateUserNameByID,
+ getQuoteLeaderBoardByID,
+ getAllQuotes,
+ getRandomQuote,
+ getProfileDataByID,
+}
\ No newline at end of file
diff --git a/server/DB/zoomzoomtypeDB.db b/server/DB/zoomzoomtypeDB.db
new file mode 100644
index 0000000..1a3e3af
Binary files /dev/null and b/server/DB/zoomzoomtypeDB.db differ
diff --git a/server/DB/zoomzoomtypeDB.db.sql b/server/DB/zoomzoomtypeDB.db.sql
new file mode 100644
index 0000000..6713a83
--- /dev/null
+++ b/server/DB/zoomzoomtypeDB.db.sql
@@ -0,0 +1,25 @@
+BEGIN TRANSACTION;
+CREATE TABLE IF NOT EXISTS "avgWPM" (
+ "avgwpmid" INTEGER NOT NULL UNIQUE,
+ "avgwmp" INTEGER NOT NULL,
+ "userID" INTEGER NOT NULL,
+ "gamesPlayed" INTEGER NOT NULL,
+ PRIMARY KEY("avgwmp" AUTOINCREMENT),
+ CONSTRAINT "userID" FOREIGN KEY("userID") REFERENCES ""
+);
+CREATE TABLE IF NOT EXISTS "leaderBoard" (
+ "leaderBoardID" INTEGER NOT NULL UNIQUE,
+ "userID" INTEGER NOT NULL,
+ "wpm" INTEGER NOT NULL,
+ PRIMARY KEY("leaderBoardID" AUTOINCREMENT),
+ CONSTRAINT "userID" FOREIGN KEY("userID") REFERENCES ""
+);
+CREATE TABLE IF NOT EXISTS "users" (
+ "userID" INTEGER NOT NULL UNIQUE,
+ "userEmail" TEXT NOT NULL UNIQUE,
+ "userPassword" TEXT NOT NULL,
+ "userUsername" TEXT NOT NULL,
+ PRIMARY KEY("userID" AUTOINCREMENT)
+);
+INSERT INTO "users" ("userID","userEmail","userPassword","userUsername") VALUES (1,'test@test.com','12345','test');
+COMMIT;
diff --git a/server/DB/zoomzoomtypeDB.sqbpro b/server/DB/zoomzoomtypeDB.sqbpro
new file mode 100644
index 0000000..6016e48
--- /dev/null
+++ b/server/DB/zoomzoomtypeDB.sqbpro
@@ -0,0 +1,2 @@
+