├── .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 |
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 | ` `,
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;
--------------------------------------------------------------------------------