├── 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 | //
16 | // );
17 |
18 | // if API news
19 | return (
20 |
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 |
125 |
126 | {data}
127 |
{this.state.message}
128 |
129 |
130 |
131 |
132 |
133 |
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 |
--------------------------------------------------------------------------------