├── server ├── controllers │ ├── cookieController.js │ ├── sessionController.js │ └── userController.js ├── models │ └── userModel.js └── server.js ├── .babelrc ├── README.md ├── src ├── index.js ├── App.js ├── Container.js ├── StockDropdown.js ├── News.js ├── Article.js ├── Data.js ├── Center.js ├── Navbar.js ├── StockBox.js └── App.css ├── .gitignore ├── public └── index.html ├── webpack.config.js └── package.json /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BattleStock 2 | Simple comparison tool for two different stocks. -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App.js'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # env variable 8 | .env 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # output from webpack 15 | dist/ 16 | 17 | # zip file for initial aws deployment 18 | *.zip 19 | 20 | # jest coverage 21 | coverage/ 22 | 23 | .vscode/ -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require('bcryptjs'); 4 | 5 | const userSchema = new Schema({ 6 | username: { type: String, required: true, unique: true }, 7 | password: { type: String, required: true }, 8 | favs: { type: Array } 9 | }); 10 | 11 | module.exports = mongoose.model('User', userSchema); 12 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import { hot } from 'react-hot-loader'; 4 | import Container from './Container.js'; 5 | import Navbar from './Navbar'; 6 | 7 | class App extends Component { 8 | render() { 9 | return ( 10 |
11 |
12 |

BattleStocks

13 |
14 |
15 | 16 |
17 |
18 | ); 19 | } 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BattleStocks 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | 4 | import StockBox from './StockBox.js'; 5 | import Center from './Center.js'; 6 | 7 | const Container = (props) => { 8 | const { username, loggedIn, favs } = props; 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 | ); 22 | }; 23 | export default Container; 24 | -------------------------------------------------------------------------------- /src/StockDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import './App.css'; 4 | import { hot } from 'react-hot-loader'; 5 | 6 | const StockDropdown = (props) => { 7 | const favStocks = []; 8 | for (let i = 0; i < props.favs.length; i++) { 9 | favStocks.push( 10 |
11 |

{props.favs[i]}

12 |
13 | ); 14 | } 15 | return ( 16 |
17 | 20 |
{favStocks}
21 |
22 | ); 23 | }; 24 | 25 | export default hot(module)(StockDropdown); 26 | -------------------------------------------------------------------------------- /src/News.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import './App.css'; 4 | import { hot } from 'react-hot-loader'; 5 | import Article from './Article.js'; 6 | 7 | class News extends Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | // console.log(this.props); 14 | const articles = []; 15 | for (let i = 0; i < this.props.news.length; i += 1) { 16 | articles.push(
); 17 | } 18 | 19 | // console.log('news props', this.props); 20 | // let headline = ''; 21 | // if (this.props.symbol.length >= 3) { 22 | // headline = `${this.props.symbol} in the news:`; 23 | // } 24 | return
{articles}
; 25 | } 26 | } 27 | 28 | export default hot(module)(News); 29 | -------------------------------------------------------------------------------- /src/Article.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import './App.css'; 4 | import { hot } from 'react-hot-loader'; 5 | 6 | const Article = (props) => { 7 | const { story } = props; 8 | // if NYT 9 | // return ( 10 | //
11 | // 12 | // {story.headline.main} 13 | // 14 | //

{story.lead_paragraph}

15 | //
16 | // ); 17 | 18 | // if API news 19 | return ( 20 |
21 | 22 |
23 |

{story.title}

24 |
25 |
26 | 27 |
28 | 29 |
30 |

{story.description.slice(0, 200) + '...'}

31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default hot(module)(Article); 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | mode: process.env.NODE_ENV, 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.(js|jsx)$/, 11 | exclude: /(node_modules|bower_components)/, 12 | loader: 'babel-loader', 13 | options: { presets: [ '@babel/preset-env', '@babel/preset-react' ] } 14 | }, 15 | { 16 | test: /\.css$/, 17 | use: [ 'style-loader', 'css-loader' ] 18 | }, 19 | { 20 | test: /\.(png|jpe?g|gif)$/i, 21 | use: [ 22 | { 23 | loader: 'file-loader' 24 | } 25 | ] 26 | } 27 | ] 28 | }, 29 | resolve: { extensions: [ '*', '.js', '.jsx' ] }, 30 | output: { 31 | path: path.resolve(__dirname, 'dist/'), 32 | publicPath: '/dist/', 33 | filename: 'bundle.js' 34 | }, 35 | devServer: { 36 | contentBase: path.join(__dirname, 'public/'), 37 | port: 3000, 38 | publicPath: '/dist/', 39 | hotOnly: true 40 | }, 41 | plugins: [ new webpack.HotModuleReplacementPlugin() ] 42 | }; 43 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/userModel'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | const userController = {}; 5 | 6 | userController.createUser = async (req, res, next) => { 7 | // Store hash in your password DB. 8 | try { 9 | res.locals.verified = false; 10 | const { username, password } = req.body; 11 | 12 | const newUser = { 13 | username: username, 14 | password: password, 15 | favs: [] 16 | }; 17 | 18 | const user = await User.create(newUser); 19 | res.locals.username = user.username; 20 | res.locals.verified = true; 21 | return next(); 22 | } catch (error) { 23 | return next(); 24 | } 25 | }; 26 | 27 | userController.verifyUser = async (req, res, next) => { 28 | res.locals.verified = false; 29 | // console.log(req.body, 'request body'); 30 | const { username, password } = req.body; 31 | 32 | try { 33 | // find the user with the associated username 34 | const user = await User.findOne({ username: username, password: password }); 35 | console.log('user found!', user); 36 | 37 | // if password worked, save the user and move on 38 | res.locals.username = user.username; 39 | res.locals.verified = true; 40 | res.locals.favs = user.favs; 41 | return next(); 42 | } catch (err) { 43 | return next(); 44 | } 45 | }; 46 | 47 | module.exports = userController; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "battlestock", 3 | "version": "1.0.0", 4 | "description": "Battle two stocks", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=development webpack", 8 | "start": "NODE_ENV=production nodemon server/server.js", 9 | "dev": "NODE_ENV=development webpack-dev-server ", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Nate Adams", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/cli": "^7.1.0", 16 | "@babel/core": "^7.1.0", 17 | "@babel/preset-env": "^7.11.5", 18 | "@babel/preset-react": "^7.10.4", 19 | "@testing-library/jest-dom": "^5.11.4", 20 | "@testing-library/react": "^11.1.0", 21 | "babel-loader": "^8.0.2", 22 | "css-loader": "^1.0.0", 23 | "file-loader": "^6.1.0", 24 | "jest": "^26.5.3", 25 | "style-loader": "^0.23.0", 26 | "test-data-bot": "^0.8.0", 27 | "webpack": "^4.19.1", 28 | "webpack-cli": "^3.1.1", 29 | "webpack-dev-server": "^3.1.8" 30 | }, 31 | "dependencies": { 32 | "@stoqey/finnhub": "0.0.7", 33 | "axios": "^0.20.0", 34 | "bcryptjs": "^2.4.3", 35 | "cookie-parser": "^1.4.5", 36 | "depcheck": "^1.2.0", 37 | "dotenv": "^8.2.0", 38 | "express": "^4.17.1", 39 | "finnhub": "^1.2.1", 40 | "fs": "0.0.1-security", 41 | "mongoose": "^5.10.15", 42 | "node-fetch": "^2.6.1", 43 | "nodemon": "^2.0.4", 44 | "passport": "^0.4.1", 45 | "react": "^16.5.2", 46 | "react-dom": "^16.5.2", 47 | "react-hot-loader": "^4.3.11", 48 | "save": "^2.4.0", 49 | "url": "^0.11.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Data.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import News from './News.js'; 4 | import StockDropdown from './StockDropdown.js'; 5 | 6 | const Data = ({ lastPrice, symbol, gains, side, news, newsRequest, favs }) => { 7 | const gainsPosOrNeg = {}; 8 | if (symbol.length) { 9 | // giving postive or negative gains different treatment 10 | 11 | Object.entries(gains).forEach((kvPair) => { 12 | const [ key, value ] = kvPair; 13 | 14 | if (value > 0) { 15 | gainsPosOrNeg[key] = `+${value}%`; 16 | } else { 17 | gainsPosOrNeg[key] = `-${value.slice(1)}%`; 18 | } 19 | }); 20 | } 21 | const stockData = []; 22 | if (favs.length) { 23 | stockData.push(); 24 | } 25 | return ( 26 |
newsRequest(symbol)} 29 | onMouseLeave={() => newsRequest(symbol)} 30 | > 31 | {stockData} 32 | 33 |

{symbol}

34 | 35 |
42 | {lastPrice} 43 |
44 | 45 |
52 | {gainsPosOrNeg.day} 53 |
54 |
61 | {gainsPosOrNeg.week} 62 |
63 |
70 | {gainsPosOrNeg.month} 71 |
72 |
79 | {gainsPosOrNeg.year} 80 |
81 |
88 | {gainsPosOrNeg.fiveYear} 89 |
90 |
91 | ); 92 | }; 93 | 94 | export default Data; 95 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | require('dotenv').config(); 5 | const path = require('path'); 6 | const bodyParser = require('body-parser'); 7 | const cookieParser = require('cookie-parser'); 8 | const mongoose = require('mongoose'); 9 | const passport = require('passport'); 10 | const fs = require('fs'); 11 | const User = require('./models/userModel.js'); 12 | const userController = require('./controllers/userController.js'); 13 | const PORT = 3000; 14 | 15 | app.use(bodyParser.urlencoded({ extended: true })); 16 | app.use(express.json()); 17 | app.use(cookieParser()); 18 | app.use(express.static('../public/')) 19 | 20 | const mongoURI = process.env.MONGO_URI; 21 | 22 | mongoose 23 | .connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }) 24 | .then((x) => { 25 | console.log('connected to mongo DB'); 26 | }) 27 | .catch((err) => { 28 | console.log('error connecting to mongo DB'); 29 | }); 30 | 31 | app.get('/', (req, res) => { 32 | // const index = fs.readFile(path.resolve(process.cwd() + '../public/index.html')); 33 | res.sendFile(path.join(__dirname, '../public/index.html')); 34 | }); 35 | 36 | app.post('/login', userController.verifyUser, (req, res) => { 37 | if (res.locals.verified) { 38 | res.send(JSON.stringify({ userVerified: true, username: res.locals.username, favs: res.locals.favs })); 39 | } else { 40 | res.send({ userVerified: false }); 41 | } 42 | }); 43 | 44 | app.post('/signup', userController.createUser, (req, res) => { 45 | if (res.locals.verified) { 46 | res.send(JSON.stringify({ userVerified: true, username: res.locals.username, favs: res.locals.favs })); 47 | } else { 48 | res.send({ userVerified: false }); 49 | } 50 | }); 51 | 52 | app.use('/dist/bundle.js', express.static(path.join(__dirname, '../dist/bundle.js'))); 53 | 54 | app.use((err, req, res, next) => { 55 | // console.log('global error handler triggered', err); 56 | const defaultErr = { 57 | log: 'Express error handler caught unknown middleware error', 58 | status: 400, 59 | message: { error: 'An error occurred' } 60 | }; 61 | 62 | const errorObj = Object.assign({}, defaultErr, err); 63 | 64 | // console.log(errorObj.log); 65 | 66 | return res.status(errorObj.status).json(errorObj.message); 67 | }); 68 | 69 | app.listen(process.env.PORT || 3000, () => { 70 | console.log(`Listening on port ${PORT }...`); 71 | }); 72 | -------------------------------------------------------------------------------- /src/Center.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | 4 | class Center extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | hoverCompare(e) { 10 | try { 11 | // we need to grab the adjacent elements 12 | const gainClass = e.target.className.split(' ')[2]; 13 | if (gainClass === 'versus') return; 14 | const isPrice = gainClass === 'currentPrice' ? true : false; 15 | 16 | const gains = document.querySelectorAll(`.${gainClass}`); 17 | const gainsMap = {}; 18 | gains.forEach((gainNum) => { 19 | if (!gainNum.classList.contains('center-category') && gainNum.textContent.length > 0) { 20 | if (gainsMap['left']) { 21 | gainsMap['right'] = gainNum; 22 | } else { 23 | gainsMap['left'] = gainNum; 24 | } 25 | } 26 | }); 27 | const { left, right } = gainsMap; 28 | let leftOperand, rightOperand, leftPrice, rightPrice; 29 | // if we're in the currentPrice category, we don't need to check for operands 30 | if (isPrice) { 31 | // console.log('currentPrice center comparison'); 32 | leftPrice = Number(left.innerText); 33 | rightPrice = Number(right.innerText); 34 | } else { 35 | leftOperand = left.innerText.charAt(0) === '-' ? '-' : ' '; 36 | rightOperand = right.innerText.charAt(0) === '-' ? '-' : ' '; 37 | leftPrice = Number(leftOperand + left.innerText.slice(1, left.innerText.length - 1)); 38 | rightPrice = Number(rightOperand + right.innerText.slice(1, right.innerText.length - 1)); 39 | } 40 | 41 | // console.log(`leftPrice: ${leftPrice} - rightPrice: ${rightPrice}`); 42 | // console.log(`left: ${left} - right: ${right}`); 43 | if (leftPrice > rightPrice) { 44 | left.classList.toggle('winner'); 45 | right.classList.toggle('loser'); 46 | } else { 47 | right.classList.toggle('winner'); 48 | left.classList.toggle('loser'); 49 | } 50 | } catch (error) { 51 | console.log('error in determining winner class. probably because data is missing on one or both sides'); 52 | } 53 | } 54 | render() { 55 | return ( 56 |
57 |
58 |
59 |

-- VS --

60 |

this.hoverCompare(e)} 62 | onMouseLeave={(e) => this.hoverCompare(e)} 63 | className="category center-category currentPrice" 64 | > 65 | Current Price 66 |

67 |

this.hoverCompare(e)} 69 | onMouseLeave={(e) => this.hoverCompare(e)} 70 | className="category center-category dayGain" 71 | > 72 | Day 73 |

74 |

this.hoverCompare(e)} 76 | onMouseLeave={(e) => this.hoverCompare(e)} 77 | className="category center-category weekGain" 78 | > 79 | Week 80 |

81 |

this.hoverCompare(e)} 83 | onMouseLeave={(e) => this.hoverCompare(e)} 84 | className="category center-category monthGain" 85 | > 86 | Month 87 |

88 |

this.hoverCompare(e)} 90 | onMouseLeave={(e) => this.hoverCompare(e)} 91 | className="category center-category yearGain" 92 | > 93 | Year 94 |

95 |

this.hoverCompare(e)} 97 | onMouseLeave={(e) => this.hoverCompare(e)} 98 | className="category center-category fiveYearGain" 99 | > 100 | 5 Year 101 |

102 |
103 |
104 | ); 105 | } 106 | } 107 | export default Center; 108 | -------------------------------------------------------------------------------- /src/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import { hot } from 'react-hot-loader'; 4 | import Container from './Container.js'; 5 | import StockDropdown from './StockDropdown'; 6 | const User = require('../server/models/userModel'); 7 | // const User = require('../models/userModel'); 8 | const bcrypt = require('bcryptjs'); 9 | const fetch = require('node-fetch'); 10 | 11 | class Navbar extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | username: '', 16 | favs: [], 17 | loggedIn: false, 18 | message: 'Sign up to save your favorite Stocks!' 19 | }; 20 | this.logIn = this.logIn.bind(this); 21 | this.signUp = this.signUp.bind(this); 22 | } 23 | 24 | logIn() { 25 | const username = document.getElementById('username-input').value; 26 | const password = document.getElementById('password-input').value; 27 | if (!password || !username) { 28 | this.setState({ 29 | ...this.state, 30 | message: "Something's wrong with your username or password! Try again" 31 | }); 32 | return; 33 | } 34 | console.log(username, password, 'in log in method inside navbar component'); 35 | const user = { 36 | username: username, 37 | password: password 38 | }; 39 | const url = `login?username=${username}&password=${password}`; 40 | fetch(url, { 41 | method: 'POST', 42 | body: JSON.stringify(user), 43 | headers: { 'Content-Type': 'application/json' } 44 | }) 45 | .then((res) => res.json()) // expecting a json response 46 | .then((data) => { 47 | // do things with data here 48 | console.log(data.favs, 'current favorites'); 49 | if (data.userVerified) { 50 | this.setState({ 51 | username: data.username, 52 | favs: data.favs, 53 | loggedIn: true, 54 | message: `User "${data.username}" successfully logged in!` 55 | }); 56 | } else { 57 | this.setState({ 58 | ...this.state, 59 | message: 'Log in failed! Try signing up.' 60 | }); 61 | } 62 | }); 63 | } 64 | signUp() { 65 | const username = document.getElementById('username-input').value; 66 | const password = document.getElementById('password-input').value; 67 | if (!password || !username) { 68 | this.setState({ 69 | ...this.state, 70 | message: "Something's wrong with your username or password! Try again" 71 | }); 72 | return; 73 | } 74 | // console.log(username, password, 'in signup method inside navbar component'); 75 | const user = { 76 | username: username, 77 | password: password 78 | }; 79 | const url = `signup?username=${username}&password=${password}`; 80 | fetch(url, { 81 | method: 'POST', 82 | body: JSON.stringify(user), 83 | headers: { 'Content-Type': 'application/json' } 84 | }) 85 | .then((res) => res.json()) // expecting a json response 86 | .then((data) => { 87 | // do things with data here 88 | console.log(data); 89 | this.setState({ 90 | username: data.username, 91 | favs: data.favs, 92 | loggedIn: true, 93 | message: `User "${data.username}" successfully signed up!` 94 | }); 95 | }) 96 | .catch((err) => { 97 | this.setState({ 98 | ...this.state, 99 | message: 'Something went wrong.' 100 | }); 101 | }); 102 | } 103 | showFavs() { 104 | console.log('show favs'); 105 | } 106 | 107 | render() { 108 | const data = []; 109 | if (!this.state.loggedIn) { 110 | data.push(); 111 | data.push(); 112 | data.push( 113 | 116 | ); 117 | data.push( 118 | 121 | ); 122 | } 123 | return ( 124 | 134 | ); 135 | } 136 | } 137 | 138 | export default hot(module)(Navbar); 139 | -------------------------------------------------------------------------------- /src/StockBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import { hot } from 'react-hot-loader'; 4 | import Data from './Data.js'; 5 | import News from './News.js'; 6 | import Navbar from './Navbar.js'; 7 | import StockDropdown from './StockDropdown.js'; 8 | 9 | const fetch = require('node-fetch'); 10 | 11 | class StockBox extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | user: '', 16 | currentSymbol: '', 17 | lastPrice: '', 18 | news: [], 19 | gains: { 20 | day: '-', 21 | week: '-', 22 | month: '-', 23 | year: '-', 24 | fiveYear: '-' 25 | } 26 | }; 27 | this.stockSearch = this.stockSearch.bind(this); 28 | this.newsRequest = this.newsRequest.bind(this); 29 | } 30 | 31 | newsRequest(symbol) { 32 | // boolean here 33 | if (this.state.news.length) return; 34 | if (symbol.length < 2) return; 35 | console.log('news request called'); 36 | 37 | // news API 38 | const today = new Date(); 39 | const url = 40 | 'http://newsapi.org/v2/everything?' + 41 | `q=${symbol}&` + 42 | `from=${today}&` + 43 | 'sortBy=popularity&' + 44 | 'apiKey=9caaf5a765a346a5b139e105a6b81947'; 45 | fetch(url) 46 | .then((response) => response.json()) 47 | .then((data) => { 48 | const articles = []; 49 | for (let i = 0; i < 10; i++) { 50 | articles.push(data.articles[i]); 51 | } 52 | 53 | // here is where we have news data to play with 54 | // console.log('article data', articles); 55 | 56 | this.setState({ 57 | ...this.state, 58 | news: articles 59 | }); 60 | // console.log(this.state); 61 | }) 62 | .catch((err) => { 63 | this.setState({ 64 | news: [] 65 | }); 66 | }); 67 | } 68 | 69 | // new york times API 70 | 71 | // const url = `https://api.nytimes.com/svc/search/v2/articlesearch.json?q=${symbol}&api-key=9BSIZH4WVjFgAa3EjV4E4klfRFnxQG9l`; 72 | // fetch(url) 73 | // .then((response) => response.json()) 74 | // .then((data) => { 75 | // console.log(data); 76 | // const articles = data.response.docs; 77 | // // here is where we have news data to play with. at least with the NYT! 78 | // console.log('article data', articles); 79 | // const newState = this.state; 80 | // newState.news = articles; 81 | // this.setState(newState); 82 | // console.log(this.state); 83 | // }) 84 | // .catch((err) => { 85 | // this.setState({ 86 | // news: [] 87 | // }); 88 | // }); 89 | // } 90 | 91 | stockSearch(e) { 92 | // here, eventually we will add some auto-fill functionality 93 | if (e.key !== 'Enter') return; 94 | console.log('stock search called'); 95 | // we can tell which side we're on by using the prop 'side' that we passed down 96 | const inputField = document.getElementById(`searchBox${this.props.side}`); 97 | const symbol = inputField.value; 98 | 99 | fetch(`http://api.marketstack.com/v1/eod?access_key=dac42c7f8b92bc6d63b3e1189fdcf7fb&symbols=${symbol}`) 100 | .then((response) => response.json()) 101 | .then((data) => { 102 | if (!data) { 103 | return; 104 | } 105 | 106 | const stockData = data.data; 107 | // current data versus one week ago 108 | const dayGain = ((stockData[0].close - stockData[1].close) / stockData[0].close * 100).toFixed(1); 109 | const weekGain = ((stockData[0].close - stockData[5].close) / stockData[0].close * 100).toFixed(1); 110 | fetch( 111 | `https://www.alphavantage.co/query?function=TIME_SERIES_MONTHLY_ADJUSTED&symbol=${symbol}&apikey=2S2GW314GW280JWA` 112 | ) 113 | .then((res) => res.json()) 114 | .then((info) => { 115 | const monthData = []; 116 | Object.values(info['Monthly Adjusted Time Series']).forEach((month) => { 117 | monthData.push(month); 118 | }); 119 | 120 | const currMonth = monthData[0]; 121 | const monthGain = ((currMonth['4. close'] - monthData[1]['4. close']) / 122 | currMonth['4. close'] * 123 | 100).toFixed(1); 124 | const yearGain = ((currMonth['4. close'] - monthData[12]['4. close']) / 125 | currMonth['4. close'] * 126 | 100).toFixed(1); 127 | const fiveYearGain = ((currMonth['4. close'] - monthData[60]['4. close']) / 128 | currMonth['4. close'] * 129 | 100).toFixed(1); 130 | this.setState({ 131 | currentSymbol: symbol.toUpperCase(), 132 | lastPrice: stockData[0].close, 133 | lastDate: stockData[0].date.slice(0, 10), 134 | gains: { 135 | day: dayGain, 136 | week: weekGain, 137 | month: monthGain, 138 | year: yearGain, 139 | fiveYear: fiveYearGain 140 | }, 141 | news: [] 142 | }); 143 | }); 144 | // this.newsRequest(this.state.currentSymbol); 145 | // console.log(stockData); 146 | }) 147 | .catch((error) => { 148 | inputField.textContent = ''; 149 | this.setState({ 150 | currentSymbol: '-', 151 | lastPrice: '-', 152 | gains: {} 153 | }); 154 | }); 155 | } 156 | 157 | render() { 158 | const sideId = `searchBox${this.props.side}`; 159 | 160 | return ( 161 |
162 |
163 | { 170 | this.stockSearch(e); 171 | }} 172 | /> 173 | 174 | 183 | 184 | {/* */} 185 |
186 |
187 | ); 188 | } 189 | } 190 | export default hot(module)(StockBox); 191 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | color: #4a4a4a; 6 | transition: 1s; 7 | font-family: 'Raleway', sans-serif; 8 | font-weight: bolder; 9 | } 10 | 11 | .title { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | max-width: 1200px; 16 | height: 150px; 17 | margin: auto; 18 | font-weight: 900; 19 | font-size: xx-large; 20 | background: rgb(155, 163, 198); 21 | background: linear-gradient(90deg, rgba(155, 163, 198, 0.95) 22%, rgba(82, 200, 161, 0.95) 78%); 22 | } 23 | 24 | .navbar-container { 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .navbar { 30 | } 31 | 32 | .container { 33 | margin-top: 50px; 34 | display: grid; 35 | width: 95vw; 36 | min-width: 500px; 37 | /* 3 total columns */ 38 | grid-template-columns: 9fr 1fr 9fr; 39 | /* 9 total rows */ 40 | 41 | grid-column-gap: 10px; 42 | 43 | border-bottom: none; 44 | border-top-left-radius: 7px; 45 | border-top-right-radius: 7px; 46 | } 47 | 48 | .innerContainer { 49 | display: grid; 50 | grid-template-rows: 60px 90px repeat(8, 50px) 8fr; 51 | grid-template-columns: 2fr 1fr 10fr 1fr 2fr; 52 | border-radius: 5px; 53 | min-width: 220px; 54 | margin: 20px; 55 | height: 100vh; 56 | } 57 | 58 | .centerContainer { 59 | display: grid; 60 | grid-template-rows: 60px 90px repeat(8, 50px); 61 | 62 | border-radius: 5px; 63 | min-width: 220px; 64 | margin: 30px; 65 | height: 600px; 66 | } 67 | 68 | .buffer { 69 | height: 75px; 70 | } 71 | #centerColumn { 72 | display: flex; 73 | justify-content: center; 74 | background-color: none; 75 | grid-column: 2 / 3; 76 | min-width: 120px; 77 | height: 600px; 78 | margin: none; 79 | } 80 | 81 | .stockBox { 82 | height: 950px; 83 | } 84 | 85 | #stockBoxLeft { 86 | background: rgb(216, 214, 232); 87 | background: linear-gradient(90deg, rgba(216, 214, 232, 1) 0%, rgba(145, 151, 177, 1) 61%); 88 | grid-column: 1 / 2; 89 | display: grid; 90 | border-radius: 5px; 91 | transition: 300ms; 92 | } 93 | 94 | #stockBoxRight { 95 | background: rgb(82, 200, 161); 96 | background: linear-gradient(90deg, rgba(82, 200, 161, 0.95) 39%, rgba(181, 227, 181, 1) 100%); 97 | grid-column: 3 / 4; 98 | display: grid; 99 | border-radius: 5px; 100 | } 101 | 102 | .searchBox { 103 | grid-row: 1 / 2; 104 | text-transform: uppercase; 105 | display: flex; 106 | justify-content: center; 107 | width: 90%; 108 | padding: 3px; 109 | margin: 5%; 110 | border: none; 111 | box-shadow: 1px 1px 1px 1px black; 112 | border-radius: 3px; 113 | transition: 500ms; 114 | text-align: center; 115 | height: 40px; 116 | font-size: larger; 117 | color: darkslategray; 118 | background: linear-gradient(#eef0f2, #aabac6); 119 | 120 | transition: 350ms ease-out; 121 | } 122 | 123 | .newsDisplay { 124 | grid-row: 11 / 12; 125 | } 126 | 127 | .searchBox:focus { 128 | outline: none; 129 | background-position: 100px; 130 | } 131 | 132 | .searchBox:hover { 133 | box-shadow: 3px 3px 1px 1px #4a4a4a; 134 | } 135 | 136 | .category { 137 | font-family: Arial, Helvetica, sans-serif; 138 | display: flex; 139 | justify-content: center; 140 | align-items: center; 141 | } 142 | .center-category:hover { 143 | font-size: 130%; 144 | text-decoration: underline; 145 | transition: 310ms; 146 | } 147 | .versus { 148 | padding-top: 20px; 149 | grid-row: 1 / 2; 150 | } 151 | .currentPrice { 152 | transition: 310ms; 153 | grid-row: 3 / 4; 154 | } 155 | 156 | .dayGain { 157 | grid-row: 4 / 5; 158 | } 159 | 160 | .weekGain { 161 | grid-row: 5 / 6; 162 | } 163 | 164 | .monthGain { 165 | grid-row: 6 / 7; 166 | } 167 | 168 | .yearGain { 169 | grid-row: 7 / 8; 170 | } 171 | 172 | .fiveYearGain { 173 | grid-row: 8 / 9; 174 | } 175 | 176 | .news { 177 | transition: 1s; 178 | display: flex; 179 | flex-wrap: wrap; 180 | overflow: auto; 181 | max-height: 780px; 182 | border-radius: 10px; 183 | } 184 | .article { 185 | margin: 25px; 186 | padding: 20px; 187 | border: 1px solid darkslategrey; 188 | border-radius: 10px; 189 | background-color: whitesmoke; 190 | box-shadow: 2px 2px 1px 1px slategray; 191 | transition: 300ms; 192 | height: 365px; 193 | width: 300px; 194 | transition: 1s; 195 | font-family: Arial, Helvetica, sans-serif; 196 | } 197 | 198 | .article:hover { 199 | box-shadow: 4px 4px 1px 1px darkslategray; 200 | background-color: floralwhite; 201 | transition: 300ms; 202 | } 203 | 204 | .article-imageContainer { 205 | display: flex; 206 | } 207 | 208 | .newsImage { 209 | margin: 20px auto; 210 | max-height: 150px; 211 | max-width: 80%; 212 | border-radius: 5px; 213 | border: 1px solid lightslategray; 214 | } 215 | 216 | .article h3 { 217 | display: -webkit-box; 218 | -webkit-line-clamp: 3; 219 | -webkit-box-orient: vertical; 220 | overflow: hidden; 221 | } 222 | 223 | .article-description { 224 | color: black; 225 | text-decoration: none; 226 | text-transform: none; 227 | display: -webkit-box; 228 | -webkit-line-clamp: 3; 229 | -webkit-box-orient: vertical; 230 | overflow: hidden; 231 | /* background-color: lightslategray; 232 | color: lightgrey; 233 | border: 1 px solid goldenrod; 234 | border-radius: 5px; */ 235 | } 236 | 237 | .article a { 238 | color: #6b82ce; 239 | } 240 | 241 | .winner { 242 | font-size: 130%; 243 | color: whitesmoke; 244 | background-color: forestgreen; 245 | box-shadow: 6px 6px 11px 6px darkgreen; 246 | transition: 500ms; 247 | border-radius: 15px; 248 | } 249 | 250 | .loser { 251 | background-color: tomato; 252 | transition: 500ms; 253 | font-size: 85%; 254 | border-radius: 15px; 255 | box-shadow: inset 3px 2px 12px 7px rosybrown; 256 | } 257 | 258 | .leftCat { 259 | grid-column: 5 / 6; 260 | } 261 | 262 | .leftCat:hover { 263 | font-weight: bolder; 264 | transition: 310ms; 265 | } 266 | 267 | .rightCat { 268 | grid-column: 1 / 2; 269 | } 270 | 271 | .rightCat:hover { 272 | font-weight: bolder; 273 | transition: 310ms; 274 | } 275 | 276 | .leftNews { 277 | grid-column: 1 / 4; 278 | grid-row: 2 / span 10; 279 | } 280 | 281 | .rightNews { 282 | grid-column: 2 / 6; 283 | grid-row: 2 / span 10; 284 | } 285 | 286 | .navbar-btn { 287 | margin: 10px; 288 | padding: 3px; 289 | border: none; 290 | box-shadow: 1px 1px 1px 1px black; 291 | border-radius: 3px; 292 | transition: 500ms; 293 | height: 40px; 294 | width: 80px; 295 | background: linear-gradient(#eef0f2, #aabac6); 296 | transition: 350ms ease-out; 297 | } 298 | 299 | .navbar-btn:hover { 300 | cursor: pointer; 301 | box-shadow: 3px 3px 1px 1px #4a4a4a; 302 | } 303 | 304 | .navbar-input { 305 | margin: 10px; 306 | padding: 3px; 307 | border: none; 308 | box-shadow: 1px 1px 1px 1px black; 309 | border-radius: 3px; 310 | transition: 500ms; 311 | text-align: center; 312 | height: 40px; 313 | font-size: larger; 314 | color: darkslategray; 315 | 316 | transition: 350ms ease-out; 317 | } 318 | 319 | .navbar-input:hover { 320 | box-shadow: 3px 3px 1px 1px #4a4a4a; 321 | } 322 | 323 | .navbar-input:focus { 324 | outline: none; 325 | background-position: 100px; 326 | } 327 | 328 | .navbar-btn:focus { 329 | outline: 1px solid gray; 330 | box-shadow: 1px 1px 1px 1px #4a4a4a; 331 | } 332 | 333 | .login-msg { 334 | display: inline; 335 | margin-left: 20px; 336 | } 337 | 338 | .dropdown { 339 | transition: 1s; 340 | float: left; 341 | overflow: hidden; 342 | border-margin: 10px; 343 | } 344 | 345 | .dropdown .dropbtn { 346 | transition: 1s; 347 | 348 | font-size: 16px; 349 | border: none; 350 | outline: none; 351 | color: white; 352 | padding: 14px 16px; 353 | color: darkslategray; 354 | background: linear-gradient(#eef0f2, #aabac6); 355 | font-family: inherit; /* Important for vertical align on mobile phones */ 356 | margin: 0; /* Important for vertical align on mobile phones */ 357 | } 358 | 359 | /* Dropdown content (hidden by default) */ 360 | .dropdown-content { 361 | transition: 1s; 362 | display: none; 363 | position: absolute; 364 | background-color: whitesmoke; 365 | min-width: 160px; 366 | box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); 367 | z-index: 1; 368 | } 369 | 370 | /* Links inside the dropdown */ 371 | .dropdown-content p { 372 | transition: 500ms; 373 | float: none; 374 | color: black; 375 | padding: 12px 16px; 376 | text-decoration: none; 377 | display: block; 378 | text-align: left; 379 | } 380 | 381 | /* Add a grey background color to dropdown links on hover */ 382 | .dropdown-content p:hover { 383 | transition: 500ms; 384 | background-color: #4a4a4a; 385 | color: whitesmoke; 386 | } 387 | 388 | /* Show the dropdown menu on hover */ 389 | .dropdown:hover .dropdown-content { 390 | display: block; 391 | } 392 | 393 | .dropdownLeft { 394 | grid-column: 1 / 2; 395 | grid-row: 1 / 2; 396 | } 397 | 398 | .dropdownRight { 399 | grid-column: 4 / 6; 400 | grid-row: 1 / 2; 401 | } 402 | --------------------------------------------------------------------------------