├── .gitignore ├── _config.yml ├── README.md ├── client ├── index.js ├── scss │ ├── _variables.scss │ ├── _loginContainer.scss │ ├── _mainContainer.scss │ └── application.scss ├── components │ ├── NavBar.jsx │ ├── Results.jsx │ ├── CodeSnippet.jsx │ └── InputField.jsx ├── App.jsx └── container │ ├── loginContainer.jsx │ └── MainContainer.jsx ├── server ├── controllers │ ├── cookieController.js │ ├── sessionController.js │ ├── oauthController.js │ ├── snippetController.js │ ├── userController.js │ └── snippet.txt ├── models │ └── snippetModel.js ├── routes │ └── api.js └── server.js ├── index.html ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeRacer 2 | 3 | Uncomment the console.logs for a better understanding of the code and how it works. 4 | Looks overwhelming at first, but isn't that bad. -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App.jsx'; 4 | import styles from './scss/application.scss'; 5 | 6 | if (module && module.hot) { 7 | module.hot.accept() 8 | } 9 | 10 | render( 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const cookieController = {}; 2 | 3 | //saves to cookie our JWT so that it can verified 4 | 5 | cookieController.setSSID = (req, res, next) => { 6 | console.log(res.locals.profile) 7 | set.cookie('ssid', res.locals.profile, { httpsOnly: true }) 8 | } 9 | 10 | 11 | module.exports = cookieController; -------------------------------------------------------------------------------- /client/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $orangeTron: #DF740C; 2 | $yellowTron: #FFE64D; 3 | $lightblueTron: #E6FFFF; 4 | $cyanTron: #18CAE6; 5 | $basestar: #0C141F; 6 | $greyTron: #D8DAE7; 7 | $deepblueTron: #34608D; 8 | $eightiesPurple: #541388; 9 | $eightiesYellow:#F9C80E; 10 | $eightiesPink:#FF4365; 11 | $eightiesCyan: #2DE2E6; 12 | $eightiesOrange:#FF6C11; 13 | $eightiesBlue: #507fa5; 14 | $eightiesDarkPurple: #663054; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CodeRacer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /server/models/snippetModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | //setting up our postgres SQL DB the same way as the DB unit 3 | const PG_URI = 'postgres://rdbpefvt:ap6jLgfucXTv7-txKVehMNOV4ncDO-UE@drona.db.elephantsql.com:5432/rdbpefvt'; 4 | 5 | //PQSQL DB table layouts 6 | // snippet: snippet_id category content max_time duration meaning 7 | // users: user_id username snippet_id highest_wpm 8 | const pool = new Pool({ 9 | connectionString: PG_URI 10 | }); 11 | 12 | // exported query with the ability to read what the request was for added debugging. 13 | module.exports = { 14 | query: (text, params, callback) => { 15 | console.log('executed query', text); 16 | return pool.query(text, params, callback); 17 | } 18 | }; -------------------------------------------------------------------------------- /server/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const sessionController = {}; 3 | const secret = 'thisisasecret' 4 | const db = require('../models/snippetModel'); 5 | 6 | //creates the jwt and saves it to our cookie 7 | sessionController.createSession = (req, res, next) => { 8 | const token = jwt.sign(res.locals.profile, secret, { expiresIn: '1h' }) 9 | res.cookie('ssid', token, { httpOnly: true }) 10 | // console.log("we made a session") 11 | return next(); 12 | } 13 | 14 | //verfies that the jwt within our cookies matches and if so stores the user's information to res.locals 15 | sessionController.verify = (req, res, next) => { 16 | // console.log('we are in the sessionController.verify') 17 | jwt.verify(req.cookies.ssid, secret, (err, result) => { 18 | if(err) { 19 | res.status(404).send('Couldn\'t verify jwt'); 20 | } else { 21 | res.locals.verifiedjwt = result; 22 | // console.log('getting through verify middleware') 23 | return next(); 24 | } 25 | }) 26 | } 27 | 28 | 29 | module.exports = sessionController; -------------------------------------------------------------------------------- /client/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react'; 2 | 3 | 4 | const NavBar = props => { 5 | let categoryArray; 6 | console.log("Has the race started?",props.isRaceStarted) 7 | // If the race has NOT started (passed from MainContainer) 8 | if (props.isRaceStarted === false) { 9 | // User has access to choose category 10 | categoryArray = props.categories.map((category, i) => ( 11 |
  • props.handleClick(category)}> { category }
  • 12 | )) 13 | } 14 | else { 15 | // If the race started, they can't change category 16 | categoryArray = props.categories.map((category, i) => ( 17 |
  • { category }
  • 18 | )) 19 | } 20 | 21 | return( 22 |
    23 | 24 |
    25 |
      26 | { categoryArray } 27 |
    28 |
    29 | 30 |
    31 | ) 32 | } 33 | 34 | export default NavBar; -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react'; 2 | import LoginContainer from './container/loginContainer.jsx'; 3 | import MainContainer from './container/MainContainer.jsx' 4 | 5 | const App = () => { 6 | const [isLoggedIn, setIsLoggedIn] = useState(false); 7 | 8 | // Checks to see if the user is logged in 9 | useEffect(() => { 10 | fetch('/verify') 11 | .then(res => { 12 | if (res.status === 200) { 13 | // console.log("WE GOT HERE and are setting the state") 14 | setIsLoggedIn(loggedIn => loggedIn = true) 15 | // console.log("THIS IS THE STATE", isLoggedIn) 16 | } 17 | // else console.log("we failed to verify the JWT") 18 | }) 19 | }) 20 | 21 | // If the user is successfully logged in: show main page 22 | // If not, show login page 23 | if (isLoggedIn) { 24 | return ( 25 |
    26 | 27 | < MainContainer /> 28 | 29 |
    30 | ) 31 | } else { 32 | return ( 33 |
    34 | 35 | < LoginContainer /> 36 | 37 |
    38 | )} 39 | } 40 | 41 | export default App; -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const snippetController = require('../controllers/snippetController'); 3 | const sessionController = require('../controllers/sessionController') 4 | const userController = require('../controllers/userController'); 5 | const router = express.Router(); 6 | 7 | 8 | //populates nav bar with our categories from our database 9 | router.get('/', snippetController.getCategories, (req, res, next) => { 10 | return res.status(200).json(res.locals.categories) 11 | }); 12 | 13 | //when clicking a category, gets a random snippet from that category and puts it into the codesnippet 14 | router.get('/:search', 15 | snippetController.getSnippet, 16 | (req, res, next) => res.status(200).json(res.locals.snippet) 17 | ); 18 | 19 | //checks if our resulting WPM is higehr than the wpm stored in the DB and responds accordingly 20 | router.post('/highScore', 21 | sessionController.verify, 22 | userController.setHighScore, 23 | (req, res, next) => res.status(200).json(res.locals.scoreBoardResponse) 24 | ) 25 | 26 | //this is the route used to populate our DB with snippets 27 | //no longer has use after popuatling SQL DB 28 | router.post('/backdoor', 29 | snippetController.createDatabase, 30 | (req, res, next) => res.status(200).send() 31 | ) 32 | 33 | module.exports = router; -------------------------------------------------------------------------------- /client/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const Results = props => { 4 | // Empty at first 5 | let resultsDiv =
    ; 6 | 7 | // If the user has finished the race (determined by props.finishedWPM existing, passed from InputField) 8 | if (Object.keys(props.finishedWPM).length) { 9 | // Show the finished WPM 10 | resultsDiv = 11 |
    12 | {props.finishedWPM.message} 13 |
    14 | } 15 | 16 | // Text is set to empty string if no category is selected 17 | let explanationText = '' 18 | // If this prop exists(activated when a category is selected by passing a prop down), show the function of the snippet 19 | if (props.content.meaning) { 20 | explanationText = "Your Snippet's Function is:" 21 | } 22 | 23 | return ( 24 |
    25 | 26 |
    27 | 28 | { resultsDiv } 29 | 30 |
    31 |
    32 | 33 |
    34 | 35 |
    36 | {explanationText} 37 |

    38 | {/* What the code that you typed does */} 39 | { props.content.meaning } 40 |

    41 |
    42 | 43 |
    44 | ) 45 | } 46 | 47 | 48 | 49 | 50 | export default Results; -------------------------------------------------------------------------------- /server/controllers/oauthController.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const oauthController = {}; 3 | 4 | //generic oauth flow for github access token 5 | oauthController.getGithubToken = (req, res, next) => { 6 | // console.log("GETTING THE TOKEN") 7 | fetch("https://github.com/login/oauth/access_token", { 8 | method: "post", 9 | headers: { 10 | "Content-Type": "application/json", 11 | "Accept": "application/json", 12 | }, 13 | body: JSON.stringify({ 14 | //our client id/secret from https://github.com/Axolotl-4/CodeRacer 15 | client_id: "3b5392180e51bf2368e3", 16 | client_secret: '7e8af0296be2fbdb1898ce9dc532f7222e7daf66', 17 | code: req.query.code, 18 | }), 19 | }) 20 | .then((res) => res.json()) 21 | .then((token) => { 22 | res.locals.id = token; 23 | return next(); 24 | }); 25 | }; 26 | 27 | //uses the oauth access token to get the user's information 28 | oauthController.getUser = (req, res, next) => { 29 | // console.log("WE ARE GETTING USER") 30 | // console.log(" THIS IS OUR ID",res.locals.id); 31 | fetch("https://api.github.com/user", { 32 | headers: { 33 | Authorization: `token ${res.locals.id.access_token}`, 34 | } 35 | }) 36 | .then((res) => res.json()) 37 | .then((result) => { 38 | // console.log("We got our User:",result) 39 | res.locals.profile = result; 40 | return next(); 41 | }) 42 | } 43 | 44 | module.exports = oauthController; -------------------------------------------------------------------------------- /client/container/loginContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | // Renders the login page 4 | const Login = () => { 5 | return ( 6 |
    7 |
    8 |

    Welcome User,



    to

    CODERACER 9 |
    10 | 11 | 12 | 13 | 14 | Log in with Github 15 | 16 |
    17 |
    18 |
    19 | ) 20 | } 21 | 22 | 23 | 24 | 25 | export default Login; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const mode = process.env.NODE_ENV 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const webpack = require('webpack') 5 | module.exports = { 6 | devServer: { 7 | publicPath: '/build/', 8 | proxy: { 9 | '/callback': 'http://localhost:3000', 10 | '/api': 'http://localhost:3000', 11 | '/verify':'http://localhost:3000' 12 | }, 13 | port: 8080, 14 | hot: true, 15 | }, 16 | entry: ['./client/index.js'], 17 | output: { 18 | path: path.resolve(__dirname, 'build'), 19 | filename: 'bundle.js', 20 | publicPath: 'http://localhost:8080/build/' 21 | }, 22 | mode, 23 | plugins: [ new MiniCssExtractPlugin(), new webpack.HotModuleReplacementPlugin() ], 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.jsx?/, 28 | exclude: /node_modules/, 29 | use: { 30 | loader: 'babel-loader', 31 | options: { 32 | presets: ['@babel/preset-env','@babel/preset-react'] 33 | } 34 | } 35 | }, 36 | 37 | { 38 | test: /\.s[ac]ss$/i, 39 | use: [ 40 | 41 | // Creates `style` nodes from JS strings 42 | //'style-loader', 43 | MiniCssExtractPlugin.loader, 44 | // Translates CSS into CommonJS 45 | 'css-loader', 46 | // Compiles Sass to CSS 47 | 'sass-loader', 48 | ], 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coderacer", 3 | "version": "1.0.0", 4 | "description": "Practice typing code alone or with other users to improve typing speed", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "set \"NODE_ENV=production\" && nodemon server/server.js", 8 | "build": "set \"NODE_ENV=production\" && webpack", 9 | "dev": "set \"NODE_ENV=development\" && concurrently \"webpack-dev-server --open\" \"nodemon server/server.js\"", 10 | "startmac": "node server/server.js", 11 | "buildmac": "nodemon server/server.js &cross-env NODE_ENV=production webpack ", 12 | "devmac": "nodemon server/server.js & cross-env NODE_ENV=development webpack-dev-server --open" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Axolotl-4/CodeRacer.git" 17 | }, 18 | "author": "Mark Alex Hang", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Axolotl-4/CodeRacer/issues" 22 | }, 23 | "homepage": "https://github.com/Axolotl-4/CodeRacer#readme", 24 | "dependencies": { 25 | "cookie-parser": "^1.4.5", 26 | "express": "^4.17.1", 27 | "jsonwebtoken": "^8.5.1", 28 | "node-fetch": "^2.6.0", 29 | "path": "^0.12.7", 30 | "pg": "^8.1.0", 31 | "react": "^16.13.1", 32 | "react-dom": "^16.13.1" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.9.6", 36 | "@babel/preset-env": "^7.9.6", 37 | "@babel/preset-react": "^7.9.4", 38 | "babel-loader": "^8.1.0", 39 | "concurrently": "^5.2.0", 40 | "cross-env": "^7.0.2", 41 | "css-loader": "^3.5.3", 42 | "mini-css-extract-plugin": "^0.9.0", 43 | "nodemon": "^2.0.3", 44 | "sass": "^1.26.5", 45 | "sass-loader": "^8.0.2", 46 | "style-loader": "^1.2.1", 47 | "webpack": "^4.43.0", 48 | "webpack-cli": "^3.3.11", 49 | "webpack-dev-server": "^3.11.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/scss/_loginContainer.scss: -------------------------------------------------------------------------------- 1 | .login{ 2 | 3 | display: flex; 4 | font-family: 'Michroma', sans-serif; 5 | justify-content: center; 6 | margin-top: 8em; 7 | 8 | } 9 | 10 | 11 | .message{ 12 | display:flex; 13 | flex-direction: column; 14 | align-items: center; 15 | box-sizing: border-box; 16 | border: 1px solid $lightblueTron; 17 | border-radius: 10px; 18 | box-shadow: 0px 0px 15px 2px lighten($cyanTron,10%), inset 0px 0px 15px 1px $cyanTron; 19 | height: 65vh; 20 | min-height: 600; 21 | min-width: 500; 22 | width: auto; 23 | justify-content: flex-start; 24 | padding-left:40px; 25 | padding-right:40px; 26 | 27 | 28 | h2{ 29 | text-align:center; 30 | user-select: none; 31 | 32 | } 33 | .title{ 34 | margin-top:50px; 35 | font-family: 'Faster One', cursive; 36 | font-size: 60px; 37 | font-weight: lighter; 38 | user-select: none; 39 | } 40 | 41 | .signIn{ 42 | $buttonColor: $basestar; 43 | box-sizing: border-box; 44 | border-radius: 6px; 45 | background-color: darken($yellowTron,5%); 46 | display:flex; 47 | color: $buttonColor; 48 | width:180px; 49 | height: 60px; 50 | margin-right: 0px; 51 | margin-top:auto; 52 | margin-bottom: auto; 53 | fill:$buttonColor; 54 | box-shadow: 0px 0px 10px 3px $orangeTron; 55 | 56 | :hover{ 57 | background-color: $lightblueTron; 58 | border-radius: 6px; 59 | box-shadow: 0px 0px 8px 2px $cyanTron; 60 | 61 | } 62 | 63 | :active{ 64 | background-color: $deepblueTron; 65 | border-radius: 6px; 66 | box-shadow: 0px 0px 8px 8px $lightblueTron,inset 0px 0px 8px 3px $lightblueTron ; 67 | } 68 | 69 | .githubButton{ 70 | user-select: none; 71 | text-decoration:none; 72 | width:100%; 73 | align-items: center; 74 | color:inherit; 75 | font-size: 100%; 76 | display:flex; 77 | justify-content: space-between; 78 | span{ 79 | width: 1fr; 80 | font-weight:800; 81 | } 82 | .githubIcon{ 83 | fill: inherit; 84 | margin-left:10px; 85 | margin-right:10px; 86 | 87 | } 88 | 89 | &:active{ 90 | fill:$lightblueTron; 91 | color:$lightblueTron; 92 | } 93 | 94 | 95 | } 96 | 97 | 98 | } 99 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const cookieparser = require('cookie-parser') 4 | 5 | const app = express(); 6 | 7 | const oauthController = require('./controllers/oauthController') 8 | const sessionController = require('./controllers/sessionController') 9 | const cookieController = require('./controllers/cookieController') 10 | const userController = require('./controllers/userController') 11 | const PORT = 3000; 12 | const apiRouter = require('./routes/api') 13 | app.use(express.json()) 14 | app.use(express.urlencoded({ extended: true })) 15 | app.use(cookieparser()); 16 | 17 | // boiler plate to get everything working. 18 | 19 | 20 | // production variable to ensure /build file is used when in production mode 21 | 22 | if (process.env.NODE_ENV === 'production') { 23 | 24 | app.use('/build', express.static(path.join(__dirname, '../build'))); 25 | // serve index.html on the route '/' 26 | app.get('/', (req, res) => { 27 | res.sendFile(path.join(__dirname, '../index.html')); 28 | }); 29 | } 30 | //Oauth flow for github 31 | app.get('/callback', 32 | oauthController.getGithubToken, 33 | oauthController.getUser, 34 | sessionController.createSession, 35 | (req, res) => { 36 | if(process.env.NODE_ENV === 'development'){ 37 | // console.log("WE ARE IN DEV ENVIRONMENT") 38 | res.redirect("localhost:8080"); 39 | 40 | } 41 | else{ 42 | res.sendFile(path.join(__dirname, '../index.html')) 43 | } 44 | }); 45 | 46 | // end of production mode stuff. 47 | 48 | 49 | // used to check the user's JWT. 50 | 51 | app.get('/verify', sessionController.verify, (req, res) => { 52 | res.status(200).send(); 53 | }) 54 | 55 | //all interactions with postgresql go through our API router 56 | app.use('/api', apiRouter) 57 | 58 | 59 | //generic error handler 60 | app.use('*', (req, res, next) => { 61 | res.status(404).send('YOU TRIED A NON EXISTENT PATH') 62 | }) 63 | 64 | 65 | 66 | 67 | // Error Handler 68 | app.use(function(err, req, res, next) { 69 | const defaultErr = { 70 | log: `'MIDDLEWARE ERROR', ${err}`, 71 | status: 400, 72 | message: { err: 'An error occurred' }, 73 | } 74 | const errorObj = Object.assign({}, defaultErr, err) 75 | console.log(errorObj.log) 76 | res.status(errorObj.status).send(JSON.stringify(errorObj.message)) 77 | }) 78 | 79 | 80 | app.listen(PORT, ()=> console.log('listening on port 3000')) 81 | module.exports = app; -------------------------------------------------------------------------------- /server/controllers/snippetController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/snippetModel'); 2 | 3 | const snippetController = {}; 4 | 5 | // query selecting from our SQL databasse 6 | // the SELECT DISTINCT make it so only unique categories are returned 7 | snippetController.getCategories = (req, res, next) => { 8 | const query = 'SELECT DISTINCT category FROM snippet' 9 | db.query(query, (err, data) => { 10 | if (err) { 11 | return next(err); 12 | } 13 | res.locals.categories = data.rows; 14 | return next(); 15 | }) 16 | }; 17 | 18 | //gets all of the snippets from our database that matches the clicked category 19 | snippetController.getSnippet = (req, res, next) => { 20 | let search = req.params.search; 21 | // console.log('we are getting the snippet with', ) 22 | const query = `SELECT * FROM snippet WHERE category = '${search}'` 23 | db.query(query, (err, data) => { 24 | if (err) { 25 | // console.log('errorin snippet query:', err) 26 | return next(err); 27 | } 28 | console.log(data.rows) 29 | res.locals.snippet = data.rows; 30 | return next(); 31 | }) 32 | }; 33 | 34 | //populates the table (snippet) with our entries to the SQL database 35 | //these were hard coded and added to the database and this function is no longer nessecary 36 | //after populating the database 37 | //feel free to use to populate the database with more snippets or turn it into an official route on the website. 38 | 39 | snippetController.createDatabase = async (req, res, next) => { 40 | const query = `INSERT INTO snippet (category, content, meaning, max_time) 41 | VALUES ($1, $2, $3, $4) 42 | RETURNING *` 43 | const promiseArray = []; 44 | snippets.forEach((element) => { 45 | let totalWords = element[1].length/5; 46 | let averageTime = Math.floor((totalWords/25) * 60) + 15; 47 | element.push(averageTime); 48 | let values = element; 49 | promiseArray.push(db.query(query, values)); 50 | }) 51 | Promise.all(promiseArray).then(res => next()) 52 | } 53 | 54 | 55 | //schema for our snippet table 56 | /* 57 | CREATE TABLE snippet ( 58 | snippet_id SERIAL PRIMARY KEY, 59 | category VARCHAR(1000), 60 | content VARCHAR(1000), 61 | meaning VARCHAR(1000), 62 | max_time INT 63 | ); 64 | */ 65 | // HTML SQL JavaScript React Express 66 | // category, content, meaning 67 | // const html = "HTML"; 68 | // const sql = "SQL"; 69 | // const javascript = "JavaScript"; 70 | // const react = "React"; 71 | // const express = "Express"; 72 | 73 | 74 | 75 | 76 | // Add snippets here in the line65 format or follow snippet.txt, which is where we stored our entries for backup 77 | // You can only add 5 at a time because we're on the free turtle plan on elephant SQL 78 | // After the array is filled, send a postman post request to 8080/api/backdoor and it'll show up on the DB 79 | const snippets = [ 80 | 81 | ]; 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | module.exports = snippetController; -------------------------------------------------------------------------------- /client/container/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react'; 2 | import NavBar from '../components/NavBar.jsx' 3 | import InputField from '../components/InputField.jsx' 4 | import CodeSnippet from '../components/CodeSnippet.jsx' 5 | 6 | class MainContainer extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | categories: [], 11 | content: {}, 12 | currentSnippet: '', 13 | inputValue : '', 14 | completedWords: [], 15 | hasRace: false, 16 | raceFinished: true, 17 | } 18 | this.handleClick = this.handleClick.bind(this) 19 | this.giveInputValue = this.giveInputValue.bind(this) 20 | this.giveCompletedWords = this.giveCompletedWords.bind(this) 21 | this.startRace = this.startRace.bind(this) 22 | this.raceFinished = this.raceFinished.bind(this) 23 | } 24 | 25 | 26 | raceFinished() { 27 | // console.log("This is our state of the race", this.state.hasRace) 28 | this.setState({hasRace: !this.state.hasRace}) 29 | } 30 | 31 | giveInputValue(inputValue) { 32 | this.setState({inputValue: inputValue}) 33 | } 34 | 35 | giveCompletedWords(completedWords) { 36 | this.setState({completedWords: completedWords}) 37 | } 38 | 39 | startRace() { 40 | // console.log("This is our state of the race", this.state.hasRace) 41 | this.setState({hasRace: !this.state.hasRace}) 42 | } 43 | 44 | // Loads all snippets of the category and randomly chooses one, also has properties other than the actual snippet (its meaning, category, max_time) 45 | handleClick(endpoint) { 46 | fetch(`/api/${endpoint}`) 47 | .then(snippet => snippet.json()) 48 | // .then(json => console.log(json)) 49 | .then(snippets => { 50 | const chosenSnippet = snippets[Math.floor(Math.random() * snippets.length)]; 51 | //console.log(chosenSnippet) 52 | this.setState({ content: chosenSnippet }) 53 | }) 54 | } 55 | 56 | // Shows the categories after the component is mounted 57 | componentDidMount() { 58 | fetch(`/api/`) 59 | .then(category => category.json()) 60 | .then(response => { 61 | const categoryArray = response.map(element => { 62 | return element.category 63 | }); 64 | this.setState({ categories: categoryArray }) 65 | }) 66 | } 67 | 68 | render() { 69 | return ( 70 |
    71 |
    CODERACER
    72 | 73 | < NavBar isRaceStarted = {this.state.hasRace} categories ={ this.state.categories } handleClick={ this.handleClick }/> 74 | 75 | < CodeSnippet content={ this.state.content } inputValue = {this.state.inputValue} completedWords = {this.state.completedWords}/> 76 | 77 | < InputField content={ this.state.content } giveCompletedWords = {this.giveCompletedWords} giveInputValue = {this.giveInputValue} startRace = {this.startRace}/> 78 | 79 |
    80 | ) 81 | } 82 | } 83 | 84 | export default MainContainer; -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/snippetModel'); 2 | const userController = {}; 3 | 4 | //schema for our users table in SQL DB 5 | // CREATE TABLE users ( 6 | // user_id SERIAL PRIMARY KEY, 7 | // username VARCHAR(50) , 8 | // snippet_id VARCHAR(50), 9 | // highest_wpm NUMERIC, 10 | // ); 11 | 12 | //sets the highest_wpm field on our users table 13 | //as well as generates the response to the frontend depending on your resulting wpm vs the highest_wpm on the DB 14 | userController.setHighScore = (req, res, next) => { 15 | // console.log("we are in the set highscore") 16 | // console.log('hopefully wpm', req.body.wordsPerMinute) 17 | // console.log("This is our verifiedJWT", res.locals.verifiedjwt) 18 | // passed down from our sessionController.verify middleware containing the username from github Oauth 19 | const search = res.locals.verifiedjwt.login 20 | const snippet_id = req.body.snippet_id 21 | //our queries onto our SQL DB 22 | const query = `SELECT * FROM users WHERE username = '${search}' AND snippet_id = '${snippet_id}'` 23 | const createWpmQuery = `INSERT INTO users(username, snippet_id, highest_wpm) 24 | VALUES($1, $2, $3)` 25 | const value = [search, snippet_id, req.body.wordsPerMinute]; 26 | const updateWPMQuery = `UPDATE users 27 | SET highest_wpm = ${req.body.wordsPerMinute} 28 | WHERE username = '${search}' AND snippet_id = '${snippet_id}'` 29 | 30 | db.query(query, (err, data) => { 31 | // console.log('data.rows', data.rows) 32 | // console.log('data', data) 33 | if (err) return next(err); 34 | //checks so see if theres currently a highest_wpm recorded for the username on the current snippet 35 | //if theres no highest_wpm or the current wpm is greater than the stored highest_wpm it will assign the current wpm to it 36 | if (data.rows.length === 0) { 37 | console.log("we have no entry") 38 | db.query(createWpmQuery, value, (err, data) => { 39 | if (err) return next(err); 40 | else { 41 | console.log("looks like we posted something") 42 | res.locals.scoreBoardResponse = { 43 | message: `CONGRATULATIONS! NEW PERSONAL RECORD! WPM: ${req.body.wordsPerMinute}`, 44 | wpm: req.body.wordsPerMinute 45 | } 46 | return next() 47 | } 48 | }) 49 | } 50 | else{ 51 | console.log("we already have an entry") 52 | if(data.rows[0].highest_wpm < req.body.wordsPerMinute) 53 | { 54 | db.query(updateWPMQuery, (err, data) => { 55 | if (err) return next(err); 56 | else { 57 | console.log("looks like we updated something") 58 | res.locals.scoreBoardResponse = { 59 | message: `CONGRATULATIONS! NEW PERSONAL RECORD! WPM:${req.body.wordsPerMinute} `, 60 | wpm: req.body.wordsPerMinute 61 | } 62 | return next() 63 | } 64 | }) 65 | } 66 | //if the highest_wpm on the DB is greater than current wpm 67 | //this is the response text that will be sent to the frontend 68 | else { 69 | console.log("looks like you have done better in the past", data.rows[0].highest_wpm) 70 | res.locals.scoreBoardResponse = { 71 | message: `TOUGH LUCK! YOU'VE DONE BETTER! PERSONAL BEST WPM: ${data.rows[0].highest_wpm} `, 72 | wpm: data.rows[0].highest_wpm 73 | } 74 | return next(); 75 | } 76 | } 77 | }) 78 | } 79 | 80 | module.exports = userController; -------------------------------------------------------------------------------- /client/components/CodeSnippet.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react'; 2 | 3 | 4 | 5 | const CodeSnippet = props => { 6 | const [currentLine,setCurrentLine] = useState(0); 7 | const [currentWordCheck,setCurrentWord] = useState(0); 8 | const [currentIndex,setCurrentIndex] = useState(0); 9 | const [completedPortion,setCompletedPortion] = useState('') 10 | 11 | // If the user hasn't selected a category, render this: 12 | if (Object.keys(props.content).length === 0) { 13 | return ( 14 |
    15 |
    16 |

    Please select a category to get started...

    17 |
    18 |
    19 | ) 20 | } 21 | else { 22 | // If the user selected a category, render this: 23 | // all of the logic for the text highlighting when typing is in here. 24 | // Brief: 25 | // has inputValue and completedWords from props, given up through the inputfield via a function that edits state of the maincontainer 26 | // inputValue is the literal value of the textArea 27 | //completedWords is an array of full words handled from the checkErrors function in inputfield 28 | // it then proceeds to use these variables to produce x many spans where x is the number chars of a particular code snippet. 29 | // this took hours to create so I suggest not messing with it unless you want to entirely overhaul the way verification is done because they are tied to gether. 30 | // Also note that the text actually has no spaces. this achieved via a 2em margin to the right of the span 31 | return ( 32 |
    33 | 34 |
    35 |

    36 | { 37 | // we process the content.content, which is the snippet displayed on the screen to eliminate all tabs or trailing spaces 38 | // we then chop it up into an array which will then form the spans of our snippet display 39 | props.content.content.trim().split(/[ \t]+/).map((word, w_idx) =>{ 40 | // we will go by word and the word's attributes are determined below 41 | let highlight = false; 42 | let currentWord = false; 43 | // first we do checks to see if the given span generated will have the above qualities 44 | 45 | // this means that the word is completed, so turn it green. Green = completed 46 | if (props.completedWords.length > w_idx) { 47 | highlight = true; 48 | } 49 | 50 | if (props.completedWords.length === w_idx) { 51 | currentWord = true; 52 | } 53 | // this is where we make our spans based on the above booleans 54 | return ( 55 | 60 | {/* We then take our word, split into individual letters and those will becomes spans */} 61 | {word.split('').map((letter, l_idx) => { 62 | /* this is the logic to see where we are on the current word and change the span qualities accordingly*/ 63 | const isCurrentWord = w_idx === props.completedWords.length; 64 | const isWronglyTyped = letter !== props.inputValue[l_idx]; 65 | const shouldBeHighlighted = l_idx < props.inputValue.length; 66 | // we chain several ternaries here to check what letter should be what color. 67 | return ( 68 | 77 | {letter} 78 | 79 | ); 80 | })} 81 | 82 | ); 83 | } 84 | ) 85 | 86 | } 87 |

    88 | 89 |
    90 | 91 |
    92 | ) 93 | } 94 | } 95 | 96 | export default CodeSnippet; 97 | -------------------------------------------------------------------------------- /client/scss/_mainContainer.scss: -------------------------------------------------------------------------------- 1 | .mainContainer{ 2 | width: 100%; 3 | height:100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | font-family: 'Michroma', sans-serif; 8 | 9 | 10 | 11 | .mainTitle{ 12 | // TRY TO MAKE THIS METAL REFLECTIVE 13 | font-family: 'Faster One', cursive; 14 | font-size: 9vw; 15 | font-weight: lighter; 16 | margin-top: none; 17 | background: -webkit-linear-gradient($eightiesYellow 30%,$eightiesOrange,$eightiesPink 70%,$eightiesPurple 90%); 18 | -webkit-background-clip: text; 19 | background-clip: text; 20 | -webkit-text-fill-color: transparent; 21 | user-select: none; 22 | } 23 | 24 | } 25 | 26 | 27 | .navBarContainer{ 28 | // border: 1px solid $lightblueTron; 29 | width:100%; 30 | display:flex; 31 | justify-content: center; 32 | 33 | .navBar{ 34 | // border: 1px solid $lightblueTron; 35 | width: auto; 36 | display: flex; 37 | 38 | #categories{ 39 | // border: 1px solid $lightblueTron; 40 | display:flex; 41 | list-style: none; 42 | padding:0; 43 | margin:0; 44 | :nth-child(1){ 45 | border-top-left-radius: 5px; 46 | border-bottom-left-radius: 5px; 47 | } 48 | :nth-last-child(1){ 49 | border-top-right-radius: 5px; 50 | border-bottom-right-radius:5px ; 51 | } 52 | 53 | .disabled{ 54 | background-color: $deepblueTron; 55 | color: $basestar; 56 | box-shadow: none; 57 | text-shadow: none; 58 | border: 1px solid $basestar; 59 | &:hover{ 60 | background-color: $deepblueTron; 61 | color: $basestar; 62 | box-shadow: none; 63 | text-shadow: none; 64 | border: 1px solid $basestar; 65 | cursor: default; 66 | } 67 | &:active{ 68 | background-color: $deepblueTron; 69 | color: $basestar; 70 | box-shadow: none; 71 | text-shadow: none; 72 | border: 1px solid $basestar; 73 | } 74 | } 75 | li{ 76 | box-sizing: border-box; 77 | padding:10px; 78 | border: 1px solid $lightblueTron; 79 | user-select: none; 80 | background-color: $basestar; 81 | box-shadow:0px 0px 5px 2px lighten($cyanTron,10%), inset 0px 0px 5px 1px $cyanTron; 82 | 83 | 84 | &:hover{ 85 | cursor: pointer; 86 | background-color: lighten($cyanTron,20%); 87 | color: $basestar; 88 | } 89 | &:active{ 90 | background-color: $eightiesPurple; 91 | color: $lightblueTron; 92 | box-shadow:0px 0px 5px 2px lighten($eightiesPink,10%), inset 0px 0px 5px 1px $eightiesPink; 93 | } 94 | } 95 | 96 | } 97 | } 98 | 99 | 100 | } 101 | 102 | .snippetContainer{ 103 | box-sizing: border-box; 104 | border: 1px solid $lightblueTron; 105 | box-shadow:0px 0px 15px 2px lighten($cyanTron,10%), inset 0px 0px 15px 1px $cyanTron ; 106 | border-radius: 6px; 107 | width:80vw; 108 | height: 50vh; 109 | min-width:700px; 110 | min-height: 500px; 111 | margin-top:2em; 112 | display: flex; 113 | overflow-y: auto; 114 | flex-direction: column; 115 | 116 | #snippet{ 117 | height:100%; 118 | width:100%; 119 | display: flex; 120 | flex-direction: column; 121 | box-sizing: border-box; 122 | padding-left:20px; 123 | padding-right:20px; 124 | font-size: 18px; 125 | 126 | 127 | 128 | .green { 129 | color:$eightiesCyan; 130 | } 131 | 132 | .red { 133 | background-color: $eightiesPink; 134 | } 135 | 136 | .underline { 137 | border-bottom: 1px solid $cyanTron; 138 | } 139 | 140 | .word{ 141 | margin-right:8px; 142 | font-family: monospace; 143 | } 144 | 145 | 146 | 147 | #snippetText{ 148 | display: flex; 149 | user-select: none; 150 | 151 | } 152 | 153 | #noText{ 154 | text-align: center; 155 | font-family: monospace; 156 | font-size:30px; 157 | user-select: none; 158 | } 159 | } 160 | } 161 | 162 | .inputContainer{ 163 | display: flex; 164 | padding: 2em; 165 | flex-direction: column; 166 | align-items: center; 167 | border: 1px solid $lightblueTron; 168 | border-radius: 6px; 169 | box-shadow:0px 0px 15px 2px lighten($cyanTron,10%), inset 0px 0px 15px 1px $cyanTron ; 170 | margin-top: 4em; 171 | margin-bottom:4em; 172 | width:50vw; 173 | min-width: 500px; 174 | 175 | #timer{ 176 | box-sizing: border-box; 177 | width: auto; 178 | height: 2em; 179 | margin-bottom: 2em; 180 | 181 | } 182 | 183 | #textInput{ 184 | resize: none; 185 | width:40%; 186 | font-size: 18px; 187 | border: 1px solid $lightblueTron; 188 | box-shadow: 0px 0px 15px 2px lighten($cyanTron,10%), inset 0px 0px 15px 1px $cyanTron ; 189 | } 190 | .resultsContainer{ 191 | display:flex; 192 | flex-direction: column; 193 | align-items:center; 194 | 195 | #explanation{ 196 | text-align: center 197 | } 198 | } 199 | 200 | } -------------------------------------------------------------------------------- /server/controllers/snippet.txt: -------------------------------------------------------------------------------- 1 | //the snippets we hardcoded to populate our DB 2 | const snippets = [ 3 | [ 4 | html, 5 | `

    My Content Heading

    My content text

    `, 6 | 'Creates a div with a h4 header and a paragraph' 7 | ], 8 | [ 9 | sql, 10 | `SELECT * FROM table WHERE id = '3'`, 11 | `Returns all columns from rows where the id is 3` 12 | ], 13 | [ 14 | javascript, 15 | `const balancedParens = (input) => { 16 | const matches = { 17 | '[': ']', 18 | '{': '}', 19 | '(': ')', 20 | }; 21 | const stack = []; 22 | for (let i = 0; i < input.length; i++) { 23 | const char = input[i]; 24 | if (matches[char]) { 25 | stack.push(char); 26 | } else if (char === ')' || char === '}' || char === ']') { 27 | if (matches[stack.pop()] !== char) { 28 | return false; 29 | } 30 | } 31 | } 32 | return !stack.length; 33 | };`, 34 | `Checks if a given string has valid parentheses. 35 | For example: '([{}()])' will yield true, while '(}{)' will yield false` 36 | ], 37 | [ 38 | sql, 39 | `INSERT INTO table (col1, col2, col3, col4) 40 | VALUES ($1, $2, $3, $4) 41 | RETURNING *`, 42 | `Creates a new entry into selected table with values of the columns determined by the $ variables and then returns what was created` 43 | ], 44 | [ 45 | react, 46 | `const element =

    Hello, world

    ; 47 | ReactDOM.render(element, document.getElementById('root'));`, 48 | 'Renders an element to the DOM' 49 | ], 50 | [ 51 | html, 52 | `picture `, 55 | 'An image that has a link anchored to it' 56 | ], 57 | [ 58 | express, 59 | `var express = require('express'); 60 | var app = express(); 61 | app.get('/', function (req, res) { 62 | res.send('

    Hello World

    '); 63 | }); 64 | app.post('/submit-data', function (req, res) { 65 | res.send('POST Request'); 66 | }); 67 | app.put('/update-data', function (req, res) { 68 | res.send('PUT Request'); 69 | }); 70 | app.delete('/delete-data', function (req, res) { 71 | res.send('DELETE Request'); 72 | }); 73 | var server = app.listen(5000, function () { 74 | console.log('Node server is running..'); 75 | });`, 76 | 'Configures routes for express.js web application framework' 77 | ], 78 | [ 79 | javascript, 80 | `var now = new Date(); 81 | var dayOfWeek = now.getDay(); // Sunday - Saturday : 0 - 6 82 | if(dayOfWeek == 5) { 83 | document.write("Have a nice weekend!"); 84 | } else if(dayOfWeek == 0) { 85 | document.write("Have a nice Sunday!"); 86 | } else { 87 | document.write("Have a nice day!"); 88 | }`, 89 | 'Multi if else statement for all the days of the week' 90 | ], 91 | [ 92 | react, 93 | `componentDidMount() { 94 | fetch(/endpoint) 95 | .then(res=> res.json()) 96 | .then(json => setState({stateAttribute: json})) 97 | }`, 98 | `Sends a fetch request to specified endpoint when the component is mounted to the webpage, 99 | it then converts the response from json and sets a state attribute to that given value` 100 | ], 101 | [ 102 | express, 103 | `sessionController.verify = (req, res, next) => { 104 | jwt.verify(req.cookies.ssid, secret, (err, result) => { 105 | if(err) { 106 | res.status(404).send('Couldn\'t verify jwt'); 107 | } else { 108 | res.locals.verifiedjwt = result; 109 | return next(); 110 | } 111 | }) 112 | }`, 113 | `Middleware that verifies a jwt before serving the webpage` 114 | ], 115 | [ 116 | express, 117 | `const express = require('express'); 118 | const path = require('path'); 119 | const cookieparser = require('cookie-parser') 120 | const app = express();`, 121 | `Imports the necessary npm libraries and creates an instance of express to set up a server` 122 | ], 123 | [ 124 | html, 125 | ` 126 | 127 | 128 | basic html 129 | 130 | 131 | 132 |

    Level One Headline

    133 |

    134 | This is a paragraph. 135 | Note that the text is automatically wrapped. 136 |

    137 | 138 | `, 139 | `Basic starting layout for an HTML file` 140 | ], 141 | [ 142 | sql, 143 | `SELECT column_name(s) 144 | FROM table1 145 | INNER JOIN table2 146 | ON table1.column_name = table2.column_name;`, 147 | `The INNER JOIN keyword selects records that have matching values in both tables.` 148 | ], 149 | [ 150 | javascript, 151 | `switch(arg) { 152 | case "hello": { 153 | console.log("hello") 154 | break 155 | } 156 | case "goodbye": { 157 | console.log("goodbye") 158 | break 159 | } 160 | default: 161 | console.log("Not a good case") 162 | }`, 163 | "Example of switch case syntax, this compares the arg against the string values and if they strictly match then it will execute that case, if it doesn't match anything, it will go to the default case" 164 | ] 165 | ]; -------------------------------------------------------------------------------- /client/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_loginContainer'; 3 | @import '_mainContainer'; 4 | 5 | // everything is imported into here. 6 | 7 | html, body{ 8 | background-color: $basestar; 9 | color: $lightblueTron; 10 | text-shadow: -1px 1px 10px $cyanTron; 11 | height:100%; 12 | margin: 0; 13 | padding:0; 14 | overflow: auto; 15 | } 16 | 17 | 18 | #root { 19 | width: 100%; 20 | height: 100%; 21 | 22 | } 23 | 24 | .containers{ 25 | width: 100%; 26 | height:100% 27 | } 28 | 29 | // animation for the flicker quality 30 | 31 | @keyframes flicker { 32 | 0% { 33 | opacity: 0.27861; 34 | } 35 | 5% { 36 | opacity: 0.34769; 37 | } 38 | 10% { 39 | opacity: 0.23604; 40 | } 41 | 15% { 42 | opacity: 0.90626; 43 | } 44 | 20% { 45 | opacity: 0.18128; 46 | } 47 | 25% { 48 | opacity: 0.83891; 49 | } 50 | 30% { 51 | opacity: 0.65583; 52 | } 53 | 35% { 54 | opacity: 0.67807; 55 | } 56 | 40% { 57 | opacity: 0.26559; 58 | } 59 | 45% { 60 | opacity: 0.84693; 61 | } 62 | 50% { 63 | opacity: 0.96019; 64 | } 65 | 55% { 66 | opacity: 0.08594; 67 | } 68 | 60% { 69 | opacity: 0.20313; 70 | } 71 | 65% { 72 | opacity: 0.71988; 73 | } 74 | 70% { 75 | opacity: 0.53455; 76 | } 77 | 75% { 78 | opacity: 0.37288; 79 | } 80 | 80% { 81 | opacity: 0.71428; 82 | } 83 | 85% { 84 | opacity: 0.70419; 85 | } 86 | 90% { 87 | opacity: 0.7003; 88 | } 89 | 95% { 90 | opacity: 0.36108; 91 | } 92 | 100% { 93 | opacity: 0.24387; 94 | } 95 | } 96 | @keyframes textShadow { 97 | 0% { 98 | text-shadow: 0.4389924193300864px 0 1px rgba(0,30,255,0.5), -0.4389924193300864px 0 1px rgba(255,0,80,0.3), 0 0 3px; 99 | } 100 | 5% { 101 | text-shadow: 2.7928974010788217px 0 1px rgba(0,30,255,0.5), -2.7928974010788217px 0 1px rgba(255,0,80,0.3), 0 0 3px; 102 | } 103 | 10% { 104 | text-shadow: 0.02956275843481219px 0 1px rgba(0,30,255,0.5), -0.02956275843481219px 0 1px rgba(255,0,80,0.3), 0 0 3px; 105 | } 106 | 15% { 107 | text-shadow: 0.40218538552878136px 0 1px rgba(0,30,255,0.5), -0.40218538552878136px 0 1px rgba(255,0,80,0.3), 0 0 3px; 108 | } 109 | 20% { 110 | text-shadow: 3.4794037899852017px 0 1px rgba(0,30,255,0.5), -3.4794037899852017px 0 1px rgba(255,0,80,0.3), 0 0 3px; 111 | } 112 | 25% { 113 | text-shadow: 1.6125630401149584px 0 1px rgba(0,30,255,0.5), -1.6125630401149584px 0 1px rgba(255,0,80,0.3), 0 0 3px; 114 | } 115 | 30% { 116 | text-shadow: 0.7015590085143956px 0 1px rgba(0,30,255,0.5), -0.7015590085143956px 0 1px rgba(255,0,80,0.3), 0 0 3px; 117 | } 118 | 35% { 119 | text-shadow: 3.896914047650351px 0 1px rgba(0,30,255,0.5), -3.896914047650351px 0 1px rgba(255,0,80,0.3), 0 0 3px; 120 | } 121 | 40% { 122 | text-shadow: 3.870905614848819px 0 1px rgba(0,30,255,0.5), -3.870905614848819px 0 1px rgba(255,0,80,0.3), 0 0 3px; 123 | } 124 | 45% { 125 | text-shadow: 2.231056963361899px 0 1px rgba(0,30,255,0.5), -2.231056963361899px 0 1px rgba(255,0,80,0.3), 0 0 3px; 126 | } 127 | 50% { 128 | text-shadow: 0.08084290417898504px 0 1px rgba(0,30,255,0.5), -0.08084290417898504px 0 1px rgba(255,0,80,0.3), 0 0 3px; 129 | } 130 | 55% { 131 | text-shadow: 2.3758461067427543px 0 1px rgba(0,30,255,0.5), -2.3758461067427543px 0 1px rgba(255,0,80,0.3), 0 0 3px; 132 | } 133 | 60% { 134 | text-shadow: 2.202193051050636px 0 1px rgba(0,30,255,0.5), -2.202193051050636px 0 1px rgba(255,0,80,0.3), 0 0 3px; 135 | } 136 | 65% { 137 | text-shadow: 2.8638780614874975px 0 1px rgba(0,30,255,0.5), -2.8638780614874975px 0 1px rgba(255,0,80,0.3), 0 0 3px; 138 | } 139 | 70% { 140 | text-shadow: 0.48874025155497314px 0 1px rgba(0,30,255,0.5), -0.48874025155497314px 0 1px rgba(255,0,80,0.3), 0 0 3px; 141 | } 142 | 75% { 143 | text-shadow: 1.8948491305757957px 0 1px rgba(0,30,255,0.5), -1.8948491305757957px 0 1px rgba(255,0,80,0.3), 0 0 3px; 144 | } 145 | 80% { 146 | text-shadow: 0.0833037308038857px 0 1px rgba(0,30,255,0.5), -0.0833037308038857px 0 1px rgba(255,0,80,0.3), 0 0 3px; 147 | } 148 | 85% { 149 | text-shadow: 0.09769827255241735px 0 1px rgba(0,30,255,0.5), -0.09769827255241735px 0 1px rgba(255,0,80,0.3), 0 0 3px; 150 | } 151 | 90% { 152 | text-shadow: 3.443339761481782px 0 1px rgba(0,30,255,0.5), -3.443339761481782px 0 1px rgba(255,0,80,0.3), 0 0 3px; 153 | } 154 | 95% { 155 | text-shadow: 2.1841838852799786px 0 1px rgba(0,30,255,0.5), -2.1841838852799786px 0 1px rgba(255,0,80,0.3), 0 0 3px; 156 | } 157 | 100% { 158 | text-shadow: 2.6208764473832513px 0 1px rgba(0,30,255,0.5), -2.6208764473832513px 0 1px rgba(255,0,80,0.3), 0 0 3px; 159 | } 160 | } 161 | .crt::after { 162 | content: " "; 163 | display: block; 164 | position: absolute; 165 | top: 0; 166 | left: 0; 167 | bottom: 0; 168 | right: 0; 169 | background: rgba(18, 16, 16, 0.1); 170 | opacity: 0; 171 | z-index: 2; 172 | pointer-events: none; 173 | animation: flicker 0.15s infinite; 174 | } 175 | .crt::before { 176 | content: " "; 177 | display: block; 178 | position: absolute; 179 | top: 0; 180 | left: 0; 181 | bottom: 0; 182 | right: 0; 183 | background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); 184 | z-index: 2; 185 | background-size: 100% 2px, 3px 100%; 186 | pointer-events: none; 187 | } 188 | // .crt { 189 | // animation: textShadow 1.6s infinite; 190 | // } 191 | 192 | //decoupled the colour diffusion from the crt base class since it hurts the eyes depending on where it is. 193 | // add or remove as you wish from dives. 194 | 195 | .crtSpecial { 196 | animation: textShadow 1.6s infinite; 197 | } -------------------------------------------------------------------------------- /client/components/InputField.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect, useRef } from 'react'; 2 | import Results from './Results.jsx' 3 | 4 | 5 | // calculatewpm = (typedCharacters/5) / endTime-startTime * 60seconds / endTime-startTime 6 | 7 | const InputField = props => { 8 | // I know I know. Why not use Redux? Because. 9 | //lots of hooks and lends to some back and forth messiness, good refactor opportunity probably? 10 | const [startTime, setStartTime] = useState(0); 11 | const [wordsPerMinute, setWordsPerMinute] = useState(0); 12 | const [completedWords, setCompletedWords] = useState([]); 13 | const [snippetSpace, setSnippetSpace] = useState([]); 14 | const [snippetProp, setSnippetProp] = useState(''); 15 | const [countDown, setCountDown] = useState(5); 16 | const [raceStarted, setRaceStarted] = useState(false); 17 | const [activeCountDown, setActiveCountDown] = useState(false); 18 | const [wpmResults, setWpmResults] = useState({}); 19 | 20 | 21 | 22 | // this is a custom made hook to allow the use of setInterval, 23 | // check the blog post of Dan Abramov about this for more info 24 | // https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 25 | const useInterval = (callback, delay) => { 26 | const savedCallback = useRef(); 27 | 28 | // Remember the latest callback. 29 | useEffect(() => { 30 | savedCallback.current = callback; 31 | }, [callback]); 32 | 33 | // Set up the interval for timer 34 | useEffect(() => { 35 | function tick() { 36 | savedCallback.current(); 37 | } 38 | if (delay !== null) { 39 | let id = setInterval(tick, delay); 40 | return () => clearInterval(id); 41 | } 42 | }, [delay]); 43 | } 44 | 45 | // literally just resets the state after race (called at the end of checkForErrors) 46 | // Also sends data to the database (WPM and snippet ID) 47 | // Sent to userController 48 | const resetState = () => { 49 | console.log("this is our final", wordsPerMinute); 50 | setStartTime(0); 51 | setCompletedWords([]); 52 | setSnippetSpace([]); 53 | setSnippetProp(''); 54 | setRaceStarted(false); 55 | setCountDown(5); 56 | props.startRace(); 57 | props.giveCompletedWords([]); 58 | props.giveInputValue(''); 59 | // console.log('You win!') 60 | document.getElementById('timer').innerHTML= 'FINISHED'; 61 | // console.log("this is our full props.content", props.content) 62 | fetch(`/api/highScore`, { 63 | method: 'POST', 64 | headers: { 65 | 'Content-Type': 'application/json' 66 | }, 67 | body: JSON.stringify({wordsPerMinute: wordsPerMinute, snippet_id: props.content.snippet_id}) 68 | }) 69 | .then((response) => response.json()) 70 | .then((response) => { 71 | setWordsPerMinute(0) 72 | //response has keys message, wpm 73 | setWpmResults(response) 74 | }) 75 | } 76 | 77 | 78 | // stop player from typing prior to start of race 79 | const isRaceOn = (e) =>{ 80 | if(!raceStarted){ 81 | e.target.value ='' 82 | props.giveInputValue('') 83 | } 84 | } 85 | 86 | 87 | 88 | // this is needed to ensure that if the content of the code snippet changes that the appropriate props are updated. 89 | useEffect(() => { 90 | if (snippetSpace.length === 0 && props.content.content) { 91 | setSnippetSpace(space=>space=props.content.content.trim().split(/[ \t]+/)); 92 | setSnippetProp(snip => snip = props.content.content ); 93 | } 94 | }) 95 | 96 | 97 | // Lets you change category after you already selected one and changes the state accordingly 98 | if (props.content.content) { 99 | // console.log('snippetSpace', snippetSpace) 100 | // console.log('split contents', props.content.content.split(' ')) 101 | if (snippetProp != props.content.content) { 102 | setSnippetSpace(space=>space=props.content.content.trim().split(/[ \t]+/)); 103 | setSnippetProp(snip => snip = props.content.content); 104 | } 105 | } 106 | 107 | 108 | const checkForErrors = (event) => { 109 | // we won't care about tabbing in our text or spaces, so we process the code snippet accordingly. 110 | // console.log("we are doing something") 111 | let snippetWords = snippetSpace; 112 | // console.log("Snippet words", snippetWords) 113 | let wholeWord = event.target.value; 114 | // console.log("wholeWord",wholeWord) 115 | let lastInput = wholeWord[wholeWord.length-1]; 116 | // console.log('lastInput', lastInput) 117 | 118 | // currentWord = array of current snippet words at index 0 119 | const currentWord = snippetWords[0]; 120 | // Gets rid of empty spaces from linebreaks, etc. 121 | if (currentWord === "" || currentWord === "\n") { 122 | let finishedWords = [...completedWords , currentWord]; 123 | event.target.value = ''; 124 | let remainingWords = [...snippetWords.slice(1)]; 125 | setSnippetSpace(remainingWords); 126 | setCompletedWords(finishedWords); 127 | props.giveInputValue(''); 128 | props.giveCompletedWords(finishedWords); 129 | } 130 | // console.log("current word",currentWord) 131 | 132 | /////////////////////////// 133 | // Actual functionality is here, checks currently typed word after pressing spacebar 134 | ///////////////////////////////////// 135 | 136 | if (lastInput === ' ' || lastInput === "\n") { 137 | // console.log("We got a match") 138 | // console.log(wholeWord.trim(),"===", currentWord) 139 | // If the inputted word(trimmed of any spaces) matches the array of snippet words at index(trimmed of any spaces) 140 | if (wholeWord.trim() === currentWord.trim()) { 141 | // console.log("WE MATCHED OUR TRIM") 142 | // slice snippetwords by first index, assigned to remainingWords 143 | let remainingWords = [...snippetWords.slice(1)]; 144 | // console.log(remainingWords) 145 | //If there are no more remaining words, call the function to resetState/end the game 146 | if (remainingWords.length === 0) { 147 | event.target.value = ''; 148 | return resetState(); 149 | } 150 | // updates finishedWords array to keep track of progress 151 | let finishedWords = [...completedWords , currentWord]; 152 | // resets textArea 153 | event.target.value = ''; 154 | // Reassign/update SnippetSpace and CompletedWords to keep track of progress 155 | setSnippetSpace(remainingWords); 156 | setCompletedWords(finishedWords); 157 | props.giveCompletedWords(finishedWords); 158 | props.giveInputValue(''); 159 | } 160 | else { 161 | event.target.value = wholeWord.trim(); 162 | } 163 | } 164 | else { 165 | props.giveInputValue(wholeWord); 166 | // update wholeWord 167 | //update lastInput 168 | } 169 | 170 | } 171 | 172 | //establishes start time upon termination of the starting clock 173 | const startRace = () => { 174 | if (startTime === 0) { 175 | setStartTime(prevTime => Date.now()); 176 | console.log("GO! CURRENT TIME IS",startTime); 177 | } 178 | setRaceStarted(raceStarted => raceStarted = true); 179 | } 180 | 181 | 182 | //calculates approximate, live wpm 183 | const calculateWPM = (event) =>{ 184 | if(raceStarted) { 185 | let inputLength = completedWords.reduce((acc,curr) => { 186 | acc = acc + curr.length; 187 | return acc; 188 | }, 0); 189 | let words = inputLength/5; 190 | let elapsedTime = Date.now() - startTime; 191 | let minute = 60000; 192 | let wpm = (words * minute) / elapsedTime; 193 | setWordsPerMinute((wpm.toFixed(2))); 194 | } 195 | } 196 | 197 | //turns on the "useinterval custom hook to begin the initial countdown" 198 | const beginCountdown = (e) => { 199 | if(raceStarted || activeCountDown) { 200 | return; 201 | } 202 | setActiveCountDown(active => active = true); 203 | props.startRace(); 204 | } 205 | // Adds the countdown timer to the page 206 | useInterval(() => { 207 | if (activeCountDown) { 208 | document.getElementById('timer').innerHTML= `Starts in ... ${countDown}`; 209 | if (countDown > 0) { 210 | setCountDown(time => time-1); 211 | } 212 | else { 213 | document.getElementById('timer').innerHTML= 'GO!'; 214 | setActiveCountDown(active => active = false); 215 | startRace(); 216 | } 217 | } 218 | 219 | }, activeCountDown ? 1000 : null); 220 | 221 | 222 | // If there is a snippet, lets you actually type in the textarea, otherwise, it's just an empty box that does nothing onclick 223 | let textArea; 224 | if (snippetProp.length) { 225 | textArea = () 229 | } 230 | else { 231 | textArea = () 232 | } 233 | 234 | return ( 235 |
    236 |
    237 | {textArea} 238 | 239 |

    240 | {/* Current WPM */} 241 | current WPM: {wordsPerMinute} 242 |

    243 | 244 | < Results finishedWPM = {wpmResults} content={ props.content } /> 245 | 246 |
    247 | ) 248 | } 249 | 250 | export default InputField; --------------------------------------------------------------------------------