├── .flaskenv
├── dev-requirements.txt
├── migrations
├── README
├── script.py.mako
├── alembic.ini
├── versions
│ ├── 20201120_150602_create_users_table.py
│ └── 20210623_154841_.py
└── env.py
├── react-app
├── .env.example
├── src
│ ├── components
│ │ ├── 404
│ │ │ ├── notFound.js
│ │ │ └── notFound.css
│ │ ├── Graph
│ │ │ ├── Graph.css
│ │ │ └── index.js
│ │ ├── Search
│ │ │ ├── search.css
│ │ │ └── index.js
│ │ ├── WatchlistTeamCard
│ │ │ ├── watchlistteamcard.css
│ │ │ └── watchlistteamcard.js
│ │ ├── auth
│ │ │ ├── LogoutButton.js
│ │ │ ├── ProtectedRoute.js
│ │ │ ├── LoginForm.js
│ │ │ ├── forms.css
│ │ │ ├── SignUpForm.js
│ │ │ └── EditUserForm.js
│ │ ├── Watchlist
│ │ │ ├── watchlist.css
│ │ │ └── index.js
│ │ ├── Home
│ │ │ ├── index.js
│ │ │ ├── LoggedOutHome.js
│ │ │ ├── home.css
│ │ │ └── LoggedInHome.js
│ │ ├── ArticleCard
│ │ │ ├── index.js
│ │ │ └── articlecard.css
│ │ ├── StockPage
│ │ │ ├── index.js
│ │ │ ├── stockpage.css
│ │ │ └── StockPageInfo.js
│ │ ├── User
│ │ │ ├── UsersList.js
│ │ │ └── index.js
│ │ ├── NavBar
│ │ │ ├── mobile.css
│ │ │ ├── index.js
│ │ │ └── mobile.js
│ │ ├── TeamStockCard
│ │ │ ├── teamstockcard.css
│ │ │ └── index.js
│ │ ├── Footer
│ │ │ └── index.js
│ │ └── TeamsList
│ │ │ ├── teams-list.css
│ │ │ └── index.js
│ ├── images
│ │ ├── graph.png
│ │ ├── robinhoop-logo-light.png
│ │ ├── robinhoop-background-ball.jpg
│ │ ├── robinhoop-background-dark.jpg
│ │ ├── robinhoop-background-court-image.jpg
│ │ ├── robinhoop-background-basketballgame.jpg
│ │ ├── robinhoop-background-basketballhoop.jpg
│ │ ├── robinhoop-background-winner-image.jpg
│ │ ├── robinhoop-background-basketballcourt.jpg
│ │ └── robinhoop-background-fans-leaving-image.jpg
│ ├── index.js
│ ├── store
│ │ ├── articles.js
│ │ ├── index.js
│ │ ├── watchlist.js
│ │ ├── buy.js
│ │ ├── stocks.js
│ │ └── session.js
│ ├── services
│ │ └── auth.js
│ ├── App.js
│ └── index.css
├── public
│ ├── favicon.ico
│ ├── placeholder.png
│ └── index.html
├── .gitignore
├── README.md
└── package.json
├── app
├── models
│ ├── db.py
│ ├── __init__.py
│ ├── watchlist.py
│ ├── history.py
│ ├── usershare.py
│ ├── team.py
│ ├── event.py
│ └── user.py
├── forms
│ ├── __init__.py
│ ├── signup_form.py
│ ├── login_form.py
│ └── edit_user_form.py
├── config.py
├── seeds
│ ├── __init__.py
│ ├── users.py
│ ├── events.py
│ └── teams.py
├── api
│ ├── buy_routes.py
│ ├── user_routes.py
│ ├── watchlist_routes.py
│ ├── team_routes.py
│ └── auth_routes.py
└── __init__.py
├── .dockerignore
├── .gitignore
├── .env.example
├── .vscode
└── settings.json
├── Dockerfile
├── requirements.txt
├── Pipfile
├── README.md
└── Pipfile.lock
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=app
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary==2.8.6
2 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/react-app/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL=http://localhost:5000
2 |
--------------------------------------------------------------------------------
/app/models/db.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | db = SQLAlchemy()
3 |
--------------------------------------------------------------------------------
/react-app/src/components/Graph/Graph.css:
--------------------------------------------------------------------------------
1 | .graph{
2 | max-height: 860px;
3 | }
4 |
--------------------------------------------------------------------------------
/react-app/src/components/Search/search.css:
--------------------------------------------------------------------------------
1 | .search-result p {
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | react-app/node_modules
2 | .venv
3 | Pipfile
4 | Pipfile.lock
5 | .env
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | __pycache__/
3 | *.py[cod]
4 | .venv
5 | .DS_Store
6 | .devcontainer/
7 | node_modules/
8 |
--------------------------------------------------------------------------------
/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/react-app/src/images/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/graph.png
--------------------------------------------------------------------------------
/react-app/public/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/public/placeholder.png
--------------------------------------------------------------------------------
/app/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from .login_form import LoginForm
2 | from .signup_form import SignUpForm
3 | from .edit_user_form import EditUserForm
4 |
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-logo-light.png
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-ball.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-ball.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-dark.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-court-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-court-image.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-basketballgame.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-basketballgame.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-basketballhoop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-basketballhoop.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-winner-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-winner-image.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-basketballcourt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-basketballcourt.jpg
--------------------------------------------------------------------------------
/react-app/src/images/robinhoop-background-fans-leaving-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NCHitsman/Python-Group-Project-Team-7/HEAD/react-app/src/images/robinhoop-background-fans-leaving-image.jpg
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | FLASK_APP=app
2 | FLASK_ENV=development
3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230
4 | DATABASE_URL=postgresql://starter_app_dev@localhost/starter_app
5 |
--------------------------------------------------------------------------------
/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from .user import User
3 | from .event import Event
4 | from .history import History
5 | from .usershare import UserShare
6 | from .team import Team
7 | from .watchlist import Watchlist
8 |
--------------------------------------------------------------------------------
/app/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class Config:
5 | SECRET_KEY = os.environ.get('SECRET_KEY')
6 | SQLALCHEMY_TRACK_MODIFICATIONS = False
7 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
8 | SQLALCHEMY_ECHO = True
9 |
--------------------------------------------------------------------------------
/react-app/src/components/WatchlistTeamCard/watchlistteamcard.css:
--------------------------------------------------------------------------------
1 | button.remove-button,
2 | button.add-button {
3 | background: rgba(0, 0, 0, 0);
4 | }
5 |
6 | button.remove-button:hover,
7 | button.add-button:hover {
8 | transition: .5s;
9 | color: #FFAA01;
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "/bin/python2",
3 | "python.linting.pylintEnabled": false,
4 | "python.linting.enabled": true,
5 | "python.linting.pycodestyleEnabled": false,
6 | "python.linting.banditEnabled": false,
7 | "python.linting.flake8Enabled": true
8 | }
--------------------------------------------------------------------------------
/react-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/LogoutButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { logout } from "../../store/session";
4 |
5 | const LogoutButton = () => {
6 | const dispatch = useDispatch();
7 | const onLogout = async (e) => {
8 | await dispatch(logout());
9 | };
10 |
11 | return Logout ;
12 | };
13 |
14 | export default LogoutButton;
15 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Route, Redirect } from 'react-router-dom';
4 |
5 | const ProtectedRoute = props => {
6 | const user = useSelector(state => state.session.user);
7 | return (
8 |
9 | {(user)? props.children : }
10 |
11 | )
12 | };
13 |
14 |
15 | export default ProtectedRoute;
16 |
--------------------------------------------------------------------------------
/react-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import './index.css';
5 | import App from './App';
6 | import configureStore from './store';
7 |
8 | const store = configureStore();
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
--------------------------------------------------------------------------------
/react-app/src/components/Watchlist/watchlist.css:
--------------------------------------------------------------------------------
1 | .watchlist {
2 | display: flex;
3 | align-items: center;
4 | height: 100%;
5 | background: #000;
6 | text-align: center;
7 | overflow-x: scroll;
8 | }
9 |
10 | .watchlist > div {
11 | height: 100%;
12 | padding: 2% 3%;
13 | }
14 |
15 | .empty-list {
16 | margin: 0 auto;
17 | padding: 5%;
18 | width: 100%;
19 | }
20 |
21 | .empty-list a button {
22 | margin-bottom: 5%;
23 | }
24 |
--------------------------------------------------------------------------------
/react-app/src/components/Home/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import LoggedOutHome from "./LoggedOutHome"
3 | import LoggedInHome from "./LoggedInHome"
4 | import { useSelector } from "react-redux"
5 |
6 | const Home = () => {
7 |
8 | const user = useSelector((state) => state.session.user)
9 |
10 |
11 | if (user) {
12 | return ( )
13 | } else {
14 | return ( )
15 | }
16 |
17 | }
18 |
19 | export default Home
20 |
--------------------------------------------------------------------------------
/react-app/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | Your React App will live here. While is development, run this application from this location using `npm start`.
4 |
5 |
6 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable in heroku!
7 |
8 | This app will be automatically built when you deploy to heroku, please see the `heroku-postbuild` script in your `express.js` applications `package.json` to see how this works.
9 |
--------------------------------------------------------------------------------
/app/models/watchlist.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Watchlist(db.Model):
5 | __tablename__ = 'watchlists'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
9 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
10 |
11 | team = db.relationship('Team')
12 | user = db.relationship('User')
13 |
14 | def to_dict(self):
15 | return {
16 | "id": self.id,
17 | "team_id": self.team_id,
18 | "user_id": self.user_id,
19 | }
20 |
--------------------------------------------------------------------------------
/app/models/history.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class History(db.Model):
5 | __tablename__ = 'history'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
9 | price = db.Column(db.Float, nullable=False)
10 | date = db.Column(db.Date, nullable=False)
11 |
12 | team = db.relationship('Team')
13 |
14 | def to_dict(self):
15 | return {
16 | "id": self.id,
17 | "team_id": self.team_id,
18 | "price": self.price,
19 | "date": self.date,
20 | }
21 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/app/models/usershare.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class UserShare(db.Model):
5 | __tablename__ = 'usershare'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | shares = db.Column(db.Integer, nullable=True)
9 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
10 | team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
11 |
12 | team = db.relationship('Team')
13 | user = db.relationship('User')
14 |
15 | def to_dict(self):
16 | return {
17 | "id": self.id,
18 | "shares": self.shares,
19 | "user_id": self.user_id,
20 | "team_id": self.team_id,
21 | }
22 |
--------------------------------------------------------------------------------
/app/forms/signup_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | print("Checking if user exits", field.data)
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if user:
12 | raise ValidationError("User is already registered.")
13 |
14 |
15 | class SignUpForm(FlaskForm):
16 | username = StringField('username', validators=[DataRequired()])
17 | email = StringField('email', validators=[DataRequired(), user_exists])
18 | password = StringField('password', validators=[DataRequired()])
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12 AS build-stage
2 |
3 | WORKDIR /react-app
4 | COPY react-app/. .
5 |
6 | # You have to set this because it should be set during build time.
7 | ENV REACT_APP_BASE_URL='https://robinhoop.herokuapp.com/'
8 |
9 | # Build our React App
10 | RUN npm install
11 | RUN npm run build
12 |
13 | FROM python:3.8
14 |
15 | # Setup Flask environment
16 | ENV FLASK_APP=app
17 | ENV FLASK_ENV=production
18 | ENV SQLALCHEMY_ECHO=True
19 |
20 | EXPOSE 8000
21 |
22 | WORKDIR /var/www
23 | COPY . .
24 | COPY --from=build-stage /react-app/build/* app/static/
25 |
26 | # Install Python Dependencies
27 | RUN pip install -r requirements.txt
28 | RUN pip install psycopg2
29 |
30 | # Run flask environment
31 | CMD gunicorn app:app
32 |
--------------------------------------------------------------------------------
/app/seeds/__init__.py:
--------------------------------------------------------------------------------
1 | from flask.cli import AppGroup
2 | from .users import seed_users, undo_users
3 | from .teams import seed_team, undo_team
4 | from .events import seed_event, undo_event
5 |
6 | # Creates a seed group to hold our commands
7 | # So we can type `flask seed --help`
8 | seed_commands = AppGroup('seed')
9 |
10 |
11 | # Creates the `flask seed all` command
12 | @seed_commands.command('all')
13 | def seed():
14 | seed_users()
15 | seed_team()
16 | seed_event()
17 | # Add other seed functions here
18 |
19 |
20 | # Creates the `flask seed undo` command
21 | @seed_commands.command('undo')
22 | def undo():
23 | undo_users()
24 | undo_team()
25 | undo_event()
26 | # Add other undo functions here
27 |
--------------------------------------------------------------------------------
/app/models/team.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Team(db.Model):
5 | __tablename__ = 'teams'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | name = db.Column(db.String, nullable=False)
9 | short_name = db.Column(db.String, nullable=False, unique=True)
10 | conference = db.Column(db.String, nullable=False)
11 | shares = db.Column(db.Integer, nullable=False)
12 | price = db.Column(db.Float, nullable=False)
13 |
14 | def to_dict(self):
15 | return {
16 | "id": self.id,
17 | "name": self.name,
18 | "short_name": self.short_name,
19 | "conference": self.conference,
20 | "shares": self.shares,
21 | "price": self.price,
22 | }
23 |
--------------------------------------------------------------------------------
/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Robinhoop
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/react-app/src/components/ArticleCard/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import "./articlecard.css"
4 |
5 | const ArticleDataCard = ({articleData}) => {
6 |
7 | const stocks = useSelector((state) => state.stocks.allStocks)
8 |
9 | const winner = stocks[articleData.winner_id]
10 |
11 | const loser = stocks[articleData.loser_id]
12 |
13 | return (
14 |
15 |
16 | {`${winner?.name} beat ${loser?.name}`}
17 |
18 |
{articleData?.winner_score} to {articleData?.loser_score}
19 |
20 | )
21 | }
22 |
23 |
24 | export default ArticleDataCard
25 |
--------------------------------------------------------------------------------
/react-app/src/components/404/notFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useHistory } from "react-router-dom"
3 | import './notFound.css'
4 |
5 | const NotFound = () => {
6 |
7 | const history = useHistory();
8 |
9 | const backToHome = () => {
10 | history.push('/')
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
404
19 |
Page Not Found
20 |
There doesn't appear to be anything here.
21 |
Back to Home
22 |
23 |
24 | )
25 | }
26 |
27 | export default NotFound;
28 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # These requirements were autogenerated by pipenv
3 | # To regenerate from the project's Pipfile, run:
4 | #
5 | # pipenv lock --requirements
6 | #
7 |
8 | -i https://pypi.org/simple
9 | alembic==1.4.3
10 | click==7.1.2
11 | dnspython==2.1.0; python_version >= '3.6'
12 | email-validator==1.1.3
13 | flask-cors==3.0.8
14 | flask-jwt-extended==3.24.1
15 | flask-login==0.5.0
16 | flask-migrate==2.5.3
17 | flask-sqlalchemy==2.4.4
18 | flask-wtf==0.14.3
19 | flask==1.1.2
20 | gunicorn==20.0.4
21 | idna==3.2; python_version >= '3.5'
22 | itsdangerous==1.1.0
23 | jinja2==2.11.2
24 | mako==1.1.3
25 | markupsafe==1.1.1
26 | pyjwt==1.7.1
27 | python-dateutil==2.8.1
28 | python-dotenv==0.14.0
29 | python-editor==1.0.4
30 | six==1.15.0
31 | sqlalchemy==1.3.19
32 | werkzeug==1.0.1
33 | wtforms==2.3.3
34 |
--------------------------------------------------------------------------------
/react-app/src/components/ArticleCard/articlecard.css:
--------------------------------------------------------------------------------
1 | .ArticleCard__cont {
2 | padding: 1%;
3 | border: 1px solid #FFAA01;
4 | border-radius: 5px;
5 | margin: 10px;
6 | background: #fff;
7 | }
8 |
9 | .ArticleCard__title {
10 | display: inline-block;
11 | font-family: 'Josefin Sans', sans-serif;
12 | text-transform: capitalize;
13 | font-size: 18px;
14 | }
15 |
16 | .ArticleCard__score {
17 | margin-top: 16px;
18 | font-family: 'Josefin Sans', sans-serif;
19 | font-size: 28px;
20 | color: #097A00;
21 | }
22 |
23 | .ArticleCard__date {
24 | font-size: 14px;
25 | font-family: 'Roboto', sans-serif;
26 | }
27 |
28 | @media screen and (max-width: 903px) {
29 | .ArticleCard__cont {
30 | height: 100%;
31 | margin-top: 1%;
32 | margin-bottom: 1%;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | click = "==7.1.2"
8 | gunicorn = "==20.0.4"
9 | itsdangerous = "==1.1.0"
10 | python-dotenv = "==0.14.0"
11 | six = "==1.15.0"
12 | Flask = "==1.1.2"
13 | Flask-Cors = "==3.0.8"
14 | Flask-SQLAlchemy = "==2.4.4"
15 | Flask-WTF = "==0.14.3"
16 | Jinja2 = "==2.11.2"
17 | MarkupSafe = "==1.1.1"
18 | SQLAlchemy = "==1.3.19"
19 | Werkzeug = "==1.0.1"
20 | WTForms = "==2.3.3"
21 | Flask-JWT-Extended = "==3.24.1"
22 | email-validator = "*"
23 | Flask-Migrate = "==2.5.3"
24 | Flask-Login = "==0.5.0"
25 | alembic = "==1.4.3"
26 | python-dateutil = "==2.8.1"
27 | python-editor = "==1.0.4"
28 | Mako = "==1.1.3"
29 | PyJWT = "==1.7.1"
30 |
31 | [dev-packages]
32 | psycopg2-binary = "==2.8.6"
33 | autopep8 = "*"
34 | pylint = "*"
35 |
36 | [requires]
37 | python_version = "3.9"
38 |
--------------------------------------------------------------------------------
/react-app/src/components/StockPage/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import {useHistory, useParams} from 'react-router-dom'
4 | import StockPageInfo from './StockPageInfo'
5 |
6 | import "./stockpage.css"
7 | import GraphCanvas from '../Graph'
8 |
9 | const StockPage= () => {
10 |
11 | const redirectTo = useHistory()
12 |
13 | const {stockId} = useParams()
14 |
15 | if (stockId > 30) {
16 | redirectTo.push('/404')
17 | }
18 |
19 |
20 | const history = useSelector((state) => state.stocks.history[stockId])
21 |
22 | return (
23 |
24 |
25 | 30 ? 1 : stockId}/>
26 |
27 | )
28 | }
29 |
30 | export default StockPage
31 |
--------------------------------------------------------------------------------
/react-app/src/components/User/UsersList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | function UsersList() {
5 | const [users, setUsers] = useState([]);
6 |
7 | useEffect(() => {
8 | async function fetchData() {
9 | const response = await fetch("/api/users/");
10 | const responseData = await response.json();
11 | setUsers(responseData.users);
12 | }
13 | fetchData();
14 | }, []);
15 |
16 | const userComponents = users.map((user) => {
17 | return (
18 |
19 | {user.username}
20 |
21 | );
22 | });
23 |
24 | return (
25 |
26 |
User List:
27 |
28 |
29 | );
30 | }
31 |
32 | export default UsersList;
33 |
--------------------------------------------------------------------------------
/react-app/src/store/articles.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const NEW_ARTICLE = 'articles/NEW_ARTICLE'
4 |
5 |
6 | const makeNewArticle = (article) => ({
7 | type: NEW_ARTICLE,
8 | payload: article
9 | })
10 |
11 |
12 | export const newArticle = (winner_id, loser_id, winner_score, loser_score) => async (dispatch) => {
13 | dispatch(makeNewArticle({
14 | winner_id,
15 | winner_score,
16 | loser_id,
17 | loser_score
18 | }))
19 | }
20 |
21 |
22 | const reducer = (state=[], action) => {
23 | const newState = [...state]
24 |
25 | switch (action.type) {
26 | case NEW_ARTICLE:
27 | newState.push(action.payload)
28 | if (newState.length > 9) {
29 | newState.shift()
30 | }
31 | return newState
32 | default:
33 | return state
34 | }
35 | }
36 |
37 | export default reducer
38 |
--------------------------------------------------------------------------------
/app/seeds/users.py:
--------------------------------------------------------------------------------
1 | from app.models import db, User
2 |
3 |
4 | # Adds a demo user, you can add other users here if you want
5 | def seed_users():
6 |
7 | demo = User(
8 | username='Demo',
9 | email='demo@aa.io',
10 | password='password'
11 | )
12 |
13 | demo1 = User(username='User', email='user@email.io', password='password')
14 | demo2 = User(username='None', email='none@noemail.io', password='password')
15 |
16 | db.session.add(demo)
17 | db.session.add(demo1)
18 | db.session.add(demo2)
19 |
20 | db.session.commit()
21 |
22 | # Uses a raw SQL query to TRUNCATE the users table.
23 | # SQLAlchemy doesn't have a built in function to do this
24 | # TRUNCATE Removes all the data from the table, and resets
25 | # the auto incrementing primary key
26 |
27 |
28 | def undo_users():
29 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;')
30 | db.session.commit()
31 |
--------------------------------------------------------------------------------
/app/models/event.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Event(db.Model):
5 | __tablename__ = 'events'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | date = db.Column(db.Date, nullable=False)
9 | winner_id = db.Column(
10 | db.Integer, db.ForeignKey('teams.id'), nullable=False
11 | )
12 | winner_score = db.Column(db.Integer, nullable=False)
13 | loser_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
14 | loser_score = db.Column(db.Integer, nullable=False)
15 |
16 | winner = db.relationship('Team', foreign_keys=[winner_id])
17 | loser = db.relationship('Team', foreign_keys=[loser_id])
18 |
19 | def to_dict(self):
20 | return {
21 | "id": self.id,
22 | "date": self.date,
23 | "winner_id": self.winner_id,
24 | "winner_score": self.winner_score,
25 | "loser_id": self.loser_id,
26 | "loser_score": self.loser_score,
27 | }
28 |
--------------------------------------------------------------------------------
/react-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import session from './session';
4 | import stocks from './stocks'
5 | import watchlist from './watchlist'
6 | import articles from './articles'
7 | import buy from './buy';
8 |
9 | const rootReducer = combineReducers({
10 | session,
11 | stocks,
12 | articles,
13 | watchlist,
14 | buy,
15 | });
16 | let enhancer;
17 |
18 | if (process.env.NODE_ENV === 'production') {
19 | enhancer = applyMiddleware(thunk);
20 | } else {
21 |
22 | const logger = require('redux-logger').default;
23 | const composeEnhancers =
24 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
25 | enhancer = composeEnhancers(applyMiddleware(thunk, logger));
26 | }
27 |
28 | const configureStore = (preloadedState) => {
29 | return createStore(rootReducer, preloadedState, enhancer);
30 | };
31 |
32 | export default configureStore;
33 |
--------------------------------------------------------------------------------
/react-app/src/components/User/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 |
4 | function User() {
5 | const [user, setUser] = useState({});
6 | // Notice we use useParams here instead of getting the params
7 | // From props.
8 | const { userId } = useParams();
9 |
10 | useEffect(() => {
11 | if (!userId) {
12 | return
13 | }
14 | (async () => {
15 | const response = await fetch(`/api/users/${userId}`);
16 | const user = await response.json();
17 | setUser(user);
18 | })();
19 | }, [userId]);
20 |
21 | if (!user) {
22 | return null;
23 | }
24 |
25 | return (
26 |
27 |
28 | User Id {userId}
29 |
30 |
31 | Username {user.username}
32 |
33 |
34 | Email {user.email}
35 |
36 |
37 | );
38 | }
39 | export default User;
40 |
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/react-app/src/components/NavBar/mobile.css:
--------------------------------------------------------------------------------
1 | nav .right.mobile {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: flex-end;
5 | margin: 8px 2%;
6 | }
7 |
8 | nav .right.mobile button#menu {
9 | display: flex;
10 | font-size: 24px;
11 | }
12 |
13 | nav .right.mobile button#menu.open {
14 | background: linear-gradient(to right, #B71C02, #ECB906);
15 | }
16 |
17 | #mobile-menu.hide {
18 | display: none;
19 | }
20 |
21 | #mobile-menu.display {
22 | transition: 5s;
23 | transition-property: display;
24 | }
25 |
26 | #mobile-menu {
27 | text-align: center;
28 | padding: 10px;
29 | border: 2px solid #fff;
30 | margin: 5px;
31 | background: #B71C02;
32 | box-shadow: 2px 2px 10px #333;
33 | }
34 |
35 | #mobile-menu a {
36 | display: block;
37 | margin-bottom: 10px;
38 | }
39 |
40 | #mobile-menu button, #mobile-menu a.nav-button {
41 | margin-left: 0px;
42 | }
43 |
44 | @media screen and (max-width: 768px) {
45 | nav {
46 | align-items: flex-start;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/forms/login_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from app.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | print("Checking if user exists", field.data)
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if not user:
12 | raise ValidationError("Email provided not found.")
13 |
14 |
15 | def password_matches(form, field):
16 | print("Checking if password matches")
17 | password = field.data
18 | email = form.data['email']
19 | user = User.query.filter(User.email == email).first()
20 | if not user:
21 | raise ValidationError("No such user exists.")
22 | if not user.check_password(password):
23 | raise ValidationError("Password was incorrect.")
24 |
25 |
26 | class LoginForm(FlaskForm):
27 | email = StringField('email', validators=[DataRequired(), user_exists])
28 | password = StringField('password', validators=[
29 | DataRequired(), password_matches])
30 |
--------------------------------------------------------------------------------
/app/api/buy_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request
2 | from flask_login import login_required, current_user
3 | from app.models import User, Team, UserShare
4 | from app import db
5 |
6 | buy_routes = Blueprint("usershares", __name__)
7 |
8 |
9 | @buy_routes.route('/')
10 | # @login_required
11 |
12 | def load_shares(id):
13 | usershare = UserShare.query.get(id)
14 | return usershare.to_dict()
15 |
16 |
17 |
18 | @buy_routes.route('/', methods=['post'])
19 | # @login_required
20 | def buy_team():
21 |
22 | data = request.json
23 | newshares = UserShare(user_id = current_user,
24 | shares = data['shares'],
25 | team_id = data['team_id'])
26 | db.session.add(newshares)
27 | db.session.commit()
28 | return newshares.to_dict()
29 |
30 |
31 | @buy_routes.route('/', methods=['delete'])
32 | # @login_required
33 | def sell_team(id):
34 | sell = UserShare.query.get(id)
35 | # if sell.user_id != current_user.id:
36 | # return {}
37 | db.session.delete(sell)
38 | db.session.commit()
39 | return {}
--------------------------------------------------------------------------------
/migrations/versions/20201120_150602_create_users_table.py:
--------------------------------------------------------------------------------
1 | """create_users_table
2 |
3 | Revision ID: ffdc0a98111c
4 | Revises:
5 | Create Date: 2020-11-20 15:06:02.230689
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'ffdc0a98111c'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table(
22 | 'users',
23 | sa.Column('id', sa.Integer(), nullable=False),
24 | sa.Column('username', sa.String(length=40), nullable=False),
25 | sa.Column('email', sa.String(length=255), nullable=False),
26 | sa.Column('hashed_password', sa.String(length=255), nullable=False),
27 | sa.PrimaryKeyConstraint('id'),
28 | sa.UniqueConstraint('email'),
29 | sa.UniqueConstraint('username')
30 | )
31 | # ### end Alembic commands ###qqqqqqqqq
32 |
33 |
34 | def downgrade():
35 | # ### commands auto generated by Alembic - please adjust! ###
36 | op.drop_table('users')
37 | # ### end Alembic commands ###
38 |
--------------------------------------------------------------------------------
/app/models/user.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 | from flask_login import UserMixin
4 |
5 |
6 | class User(db.Model, UserMixin):
7 | __tablename__ = 'users'
8 |
9 | id = db.Column(db.Integer, primary_key=True)
10 | username = db.Column(db.String(40), nullable=False, unique=True)
11 | email = db.Column(db.String(255), nullable=False, unique=True)
12 | hashed_password = db.Column(db.String(255), nullable=False)
13 |
14 | @property
15 | def password(self):
16 | return self.hashed_password
17 |
18 | @password.setter
19 | def password(self, password):
20 | self.hashed_password = generate_password_hash(password)
21 |
22 | def check_password(self, password):
23 | return check_password_hash(self.password, password)
24 |
25 | def to_dict(self):
26 | return {
27 | "id": self.id,
28 | "username": self.username,
29 | "email": self.email
30 | }
31 |
32 | def update(self, **kwargs):
33 | for key, value in kwargs.items():
34 | if hasattr(self, key):
35 | setattr(self, key, value)
36 |
--------------------------------------------------------------------------------
/react-app/src/services/auth.js:
--------------------------------------------------------------------------------
1 | export const authenticate = async() => {
2 | const response = await fetch('/api/auth/',{
3 | headers: {
4 | 'Content-Type': 'application/json'
5 | }
6 | });
7 | return await response.json();
8 | }
9 |
10 | export const login = async (email, password) => {
11 | const response = await fetch('/api/auth/login', {
12 | method: 'POST',
13 | headers: {
14 | 'Content-Type': 'application/json'
15 | },
16 | body: JSON.stringify({
17 | email,
18 | password
19 | })
20 | });
21 | return await response.json();
22 | }
23 |
24 | export const logout = async () => {
25 | const response = await fetch("/api/auth/logout", {
26 | headers: {
27 | "Content-Type": "application/json",
28 | }
29 | });
30 | return await response.json();
31 | };
32 |
33 |
34 | export const signUp = async (username, email, password) => {
35 | const response = await fetch("/api/auth/signup", {
36 | method: "POST",
37 | headers: {
38 | "Content-Type": "application/json",
39 | },
40 | body: JSON.stringify({
41 | username,
42 | email,
43 | password,
44 | }),
45 | });
46 | return await response.json();
47 | }
--------------------------------------------------------------------------------
/react-app/src/components/WatchlistTeamCard/watchlistteamcard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import {useHistory} from 'react-router-dom';
4 | import { removeFromWatchlist } from '../../store/watchlist';
5 | import "../TeamStockCard/teamstockcard.css";
6 | import "./watchlistteamcard.css";
7 |
8 | const WatchlistTeamCard = ({teamId}) => {
9 | const dispatch = useDispatch()
10 | const history = useHistory()
11 |
12 | let team = useSelector(state => state.stocks.allStocks[`${teamId}`])
13 | let userId = useSelector(state => state.session.user.id)
14 |
15 |
16 | return (
17 | <>
18 | history.push(`/stock/${team.id}`)}
20 | >
21 | {team.name}
22 | ({team.short_name})
23 | Conference: {team.conference}
24 | ${team.price}
25 |
26 |
27 | >
28 | )
29 | }
30 |
31 | export default WatchlistTeamCard
32 |
--------------------------------------------------------------------------------
/app/forms/edit_user_form.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField
3 | from wtforms.validators import DataRequired, Email, ValidationError
4 | from flask_login import current_user
5 | from app.models import User
6 |
7 |
8 | def user_exists(form, field):
9 | print("Checking if user exits", field.data)
10 | email = field.data
11 | user = User.query.filter(User.email == email).first()
12 | if user and user.id != current_user.id:
13 | raise ValidationError("User is already registered.")
14 |
15 |
16 | def password_matches(form, field):
17 | print("Checking if password matches")
18 | password = form.data["password"]
19 | repeat_password = form.data["repeat_password"]
20 | if password != repeat_password:
21 | raise ValidationError("Passwords do not match.")
22 |
23 |
24 | class EditUserForm(FlaskForm):
25 | username = StringField('username', validators=[DataRequired()])
26 | email = StringField('email', validators=[DataRequired(), user_exists])
27 | password = StringField('password', validators=[DataRequired()])
28 | repeat_password = StringField("repeat_password", validators=[
29 | DataRequired(), password_matches
30 | ])
31 |
--------------------------------------------------------------------------------
/react-app/src/components/TeamStockCard/teamstockcard.css:
--------------------------------------------------------------------------------
1 | button.TeamStockCard__cont {
2 | transition: .5s;
3 | min-width: 150px;
4 | min-height: 160px;
5 | margin: 1%;
6 | border: 1px solid #FFAA01;
7 | border-radius: 5px;
8 | color: #000;
9 | background: rgba(250, 250, 250, .9);
10 | }
11 |
12 | button.TeamStockCard__cont:hover {
13 | transition: .5s;
14 | min-width: 150px;
15 | min-height: 160px;
16 | margin: 1%;
17 | border: 1px solid #FFAA01;
18 | border-radius: 5px;
19 | color: #000;
20 | background: rgba(250, 250, 250, 1);
21 | cursor: pointer;
22 | }
23 |
24 | .TeamStockCard__name {
25 | display: inline-block;
26 | font-family: 'Josefin Sans', sans-serif;
27 | text-transform: capitalize;
28 | font-size: 21px;
29 | }
30 |
31 | .TeamStockCard__short_name {
32 | display: inline-block;
33 | font-family: 'Josefin Sans', sans-serif;
34 | text-transform: uppercase;
35 | font-size: 14px;
36 | }
37 |
38 | .TeamStockCard__conference {
39 | font-family: 'Roboto', sans-serif;
40 | font-style: italic;
41 | font-size: 14px;
42 | }
43 |
44 | .TeamStockCard__price {
45 | margin-top: 16px;
46 | font-family: 'Josefin Sans', sans-serif;
47 | font-size: 28px;
48 | color: #097A00;
49 | }
50 |
--------------------------------------------------------------------------------
/app/api/user_routes.py:
--------------------------------------------------------------------------------
1 | from app.api.auth_routes import validation_errors_to_error_messages
2 | from flask import Blueprint, jsonify, request
3 | from flask_login import login_required, current_user
4 | from app.models import User
5 | from app.forms import EditUserForm
6 | from app import db
7 |
8 | user_routes = Blueprint('users', __name__)
9 |
10 |
11 | @user_routes.route('/')
12 | @login_required
13 | def users():
14 | users = User.query.all()
15 | return {"users": [user.to_dict() for user in users]}
16 |
17 |
18 | @user_routes.route('/edit-account', methods=["POST"])
19 | @login_required
20 | def edit_user():
21 | user = User.query.get(current_user.id)
22 | form = EditUserForm()
23 | form['csrf_token'].data = request.cookies['csrf_token']
24 | if form.validate_on_submit():
25 | user.update(
26 | username=form.data['username'],
27 | email=form.data['email'],
28 | password=form.data['password']
29 | )
30 | db.session.add(user)
31 | db.session.commit()
32 | return user.to_dict()
33 | return {"errors": validation_errors_to_error_messages(form.errors)}, 401
34 |
35 |
36 | @user_routes.route('/')
37 | @login_required
38 | def user(id):
39 | user = User.query.get(id)
40 | return user.to_dict()
41 |
--------------------------------------------------------------------------------
/react-app/src/components/Watchlist/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux"
3 |
4 | import TeamStockCard from "../TeamStockCard";
5 | import "./watchlist.css"
6 |
7 |
8 | export const Watchlist = () => {
9 |
10 | const list = useSelector(state => state.watchlist)
11 | const allStocks = useSelector(state => state.stocks.allStocks)
12 | let stocks = Object.values(list).map((watchListItem) => {
13 | return allStocks[watchListItem.team_id]
14 | })
15 |
16 | return (
17 |
18 |
Watchlist
19 |
20 | {!stocks.length == 0 ? stocks.map(stock => {
21 | return (
22 |
23 |
24 |
25 | )
26 | }):
27 |
28 |
Your watchlist is empty.
29 |
Add Stocks
30 |
}
31 |
32 |
33 | )
34 | }
35 |
36 | export default Watchlist;
37 |
--------------------------------------------------------------------------------
/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "chart.js": "^3.3.2",
10 | "http-proxy-middleware": "^1.0.5",
11 | "react": "^17.0.0",
12 | "react-chartjs-2": "^3.0.3",
13 | "react-dom": "^17.0.0",
14 | "react-redux": "^7.2.4",
15 | "react-router-dom": "^5.2.0",
16 | "react-scripts": "3.4.3",
17 | "redux": "^4.1.0",
18 | "redux-logger": "^3.0.6",
19 | "redux-thunk": "^2.3.0"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.3%",
33 | "not ie 11",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version",
41 | ">0.3%",
42 | "not ie 11",
43 | "not dead",
44 | "not op_mini all"
45 | ]
46 | },
47 | "proxy": "http://localhost:5000"
48 | }
49 |
--------------------------------------------------------------------------------
/app/api/watchlist_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request
2 | from flask_login import login_required, current_user
3 | from app.models import User, Team, Watchlist
4 | from app import db
5 |
6 | watchlist_routes = Blueprint("watchlists", __name__)
7 |
8 |
9 | @watchlist_routes.route('/')
10 | @login_required
11 | def get_watchlist(id):
12 | lists = Watchlist.query.filter(Watchlist.user_id == id).all()
13 | return jsonify([item.to_dict() for item in lists])
14 |
15 |
16 | @watchlist_routes.route('/add//', methods=['POST'])
17 | @login_required
18 | def new_watchlist(userId, teamId):
19 |
20 | new_watch = Watchlist(
21 | team_id = teamId,
22 | user_id = userId
23 | )
24 |
25 | db.session.add(new_watch)
26 | db.session.commit()
27 |
28 | return new_watch.to_dict()
29 |
30 |
31 | @watchlist_routes.route('/delete//', methods=['DELETE'])
32 | @login_required
33 | def delete_watchlist(userId, teamId):
34 | team = Watchlist.query.filter(Watchlist.team_id == teamId and Watchlist.user_id == userId).first()
35 | jsonify(team.to_dict())
36 | db.session.delete(team)
37 | db.session.commit()
38 | lists = Watchlist.query.filter(Watchlist.user_id == userId).all()
39 | return jsonify([item.to_dict() for item in lists])
40 |
--------------------------------------------------------------------------------
/react-app/src/components/404/notFound.css:
--------------------------------------------------------------------------------
1 | .container {
2 | height: 100%;
3 | background: #000;
4 | display: flex;
5 | flex-wrap: wrap;
6 | color: #fff;
7 | }
8 |
9 | #error-list > div {
10 | background: rgba(353, 98, 41, .3) ;
11 | padding: 5px;
12 | width: 100%;
13 | font-family: 'Roboto', sans-serif;
14 | color: #fff;
15 | }
16 |
17 | .left-side {
18 | background-position: center center;
19 | background-size: cover;
20 | background-repeat: no-repeat;
21 | width: 50%;
22 | }
23 |
24 | .left-side.not-found {
25 | background-image: url("../../images/robinhoop-background-fans-leaving-image.jpg");
26 | }
27 |
28 | #not-found-message.right-side {
29 | width: 50%;
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: center;
33 | align-items: center;
34 | }
35 |
36 | .edit-wrapper {
37 | display: flex;
38 | justify-content: center;
39 | }
40 |
41 |
42 | @media screen and (max-width: 767px) {
43 | .left-side {
44 | width: 100%;
45 | }
46 |
47 | #not-found-message.right-side {
48 | width: 100%;
49 | justify-content: center;
50 | }
51 |
52 | form > div {
53 | max-width: 95%;
54 | }
55 | }
56 |
57 | @media screen and (max-width: 320px) {
58 |
59 | .right-side {
60 | max-width: 80%;
61 | margin: 0 auto;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/seeds/events.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Event
2 |
3 |
4 | def seed_event():
5 |
6 | # event1 = Event(
7 | # date='2021-01-13',
8 | # winner_id='18',
9 | # winner_score='113',
10 | # loser_id='6',
11 | # loser_score='105'
12 | # )
13 | # event2 = Event(
14 | # date='2021-02-11',
15 | # winner_id='16',
16 | # winner_score='98',
17 | # loser_id='12',
18 | # loser_score='85'
19 | # )
20 | # event3 = Event(
21 | # date='2021-02-24',
22 | # winner_id='6',
23 | # winner_score='106',
24 | # loser_id='9',
25 | # loser_score='99'
26 | # )
27 | # event4 = Event(
28 | # date='2021-03-07',
29 | # winner_id='15',
30 | # winner_score='96',
31 | # loser_id='30',
32 | # loser_score='78'
33 | # )
34 | # event5 = Event(
35 | # date='2021-03-21',
36 | # winner_id='24',
37 | # winner_score='87',
38 | # loser_id='12',
39 | # loser_score='79'
40 | # )
41 |
42 | # db.session.add(event1)
43 | # db.session.add(event2)
44 | # db.session.add(event3)
45 | # db.session.add(event4)
46 | # db.session.add(event5)
47 |
48 | db.session.commit()
49 |
50 |
51 | def undo_event():
52 | db.session.execute('TRUNCATE Events RESTART IDENTITY CASCADE;')
53 | db.session.commit()
54 |
--------------------------------------------------------------------------------
/app/api/team_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request
2 | from flask_login import login_required, current_user
3 | from app.models import User, Team, Event, UserShare, History
4 | from app import db
5 | from sqlalchemy import desc
6 | import datetime
7 |
8 | team_routes = Blueprint('teams', __name__)
9 |
10 |
11 | @team_routes.route('/')
12 | def stock(stockId):
13 | stocks = Team.query.get(stockId)
14 | return stocks.to_dict()
15 |
16 |
17 | @team_routes.route('/')
18 | def teams():
19 | teams = Team.query.all()
20 | return {"teams": [team.to_dict() for team in teams]}
21 |
22 |
23 | @team_routes.route('/editPrice/', methods=['PUT'])
24 | def edit_teams(stockId):
25 |
26 | price = request.json['price']
27 | currentTeam = Team.query.get(stockId)
28 |
29 | if price < 0:
30 | currentTeam.price = 0
31 | else:
32 | currentTeam.price = price
33 | db.session.commit()
34 |
35 | teams = Team.query.all()
36 | return {"teams": [team.to_dict() for team in teams]}
37 |
38 |
39 | @team_routes.route('/userShares//')
40 | def userShare(userId, stockId):
41 |
42 | userSharex = UserShare.query.filter(
43 | UserShare.user_id == userId
44 | ).filter(
45 | UserShare.team_id == stockId
46 | ).one_or_none()
47 |
48 | if userSharex is None:
49 | return jsonify('not found')
50 |
51 | return userSharex.to_dict()
52 |
--------------------------------------------------------------------------------
/react-app/src/components/TeamStockCard/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import "./teamstockcard.css"
3 | import {useHistory} from 'react-router-dom'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import { removeFromWatchlist } from '../../store/watchlist'
6 |
7 | const TeamStockCard = ({teamStockData, watchlist}) => {
8 | const dispatch = useDispatch()
9 | const history = useHistory()
10 |
11 | const user = useSelector(state => state.session.user)
12 |
13 | const formatter = new Intl.NumberFormat('en-US', {
14 | style: 'currency',
15 | currency: 'USD'
16 | });
17 |
18 | const removeItem = () => {
19 | dispatch(removeFromWatchlist(user.id, teamStockData.id))
20 | }
21 |
22 | return (
23 | <>
24 | history.push(`/stock/${teamStockData.id}`)}
26 | >
27 | {teamStockData.name}
28 | ({teamStockData.short_name})
29 | Conference: {teamStockData.conference}
30 | {formatter.format(teamStockData.price)}
31 |
32 | {watchlist &&
33 | removeItem()}
35 | >
36 | Remove
37 | }
38 | >
39 | )
40 | }
41 |
42 | export default TeamStockCard
43 |
--------------------------------------------------------------------------------
/react-app/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logo from '../../images/robinhoop-logo-light.png';
3 |
4 | const Footer = () => {
5 |
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
About Robinhoop
14 |
Robinhoop is a simulated basketball stock market, not an actual stock market. No real currency is won, lost, or exchanged by using this app. It's mostly just for fun.
15 |
16 |
17 |
Site by:
18 |
Andrew Musta
19 | Nathan Mac
20 | Noah Carmichael-Hitsman
21 | Jacob Leonhardt
22 |
23 |
24 |
25 |
26 |
©2021 Robinhoop
27 |
28 |
29 | )
30 | }
31 |
32 | export default Footer
33 |
--------------------------------------------------------------------------------
/react-app/src/components/Graph/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Graph.css'
3 | import { Line } from 'react-chartjs-2';
4 |
5 | const GraphCanvas = ({ history }) => {
6 |
7 |
8 | const down = (context, value) => context.p0.parsed.y > context.p1.parsed.y ? value : undefined;
9 |
10 | const data = {
11 | labels: [],
12 | datasets: [
13 | {
14 | label: 'Price',
15 | data: [],
16 | fill: false,
17 | backgroundColor: 'black',
18 | borderColor: 'green',
19 | segment: {
20 | borderColor: context => down(context, 'red'),
21 | }
22 | },
23 | ],
24 | };
25 |
26 | let lastHistory;
27 | for (let i = 0; i < history?.length; i++) {
28 | if (history[i].price !== lastHistory) {
29 | data.labels.push(history[i].date)
30 | data.datasets[0].data.push(history[i].price)
31 | lastHistory = history[i].price
32 | }
33 | }
34 |
35 | const options = {
36 | scales: {
37 | yAxes: [
38 | {
39 | ticks: {
40 | beginAtZero: true,
41 | },
42 | },
43 | ],
44 | xAxes: [
45 | {
46 | type: "realtime",
47 | realtime: {
48 | onRefresh: function() {
49 | data.datasets[0].data.unshift(5000);
50 | },
51 | delay: 1
52 | }
53 | }
54 | ]
55 | },
56 | animation: {
57 | duration: 0
58 | },
59 | elements: {
60 | point: {
61 | radius: 1.5
62 | }
63 | },
64 | reponsive: true
65 | };
66 |
67 | return (
68 | <>
69 |
70 | >
71 | )
72 | }
73 |
74 | export default GraphCanvas
75 |
--------------------------------------------------------------------------------
/react-app/src/components/NavBar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import LogoutButton from '../auth/LogoutButton';
5 | import SearchBar from "../Search/index";
6 | import logo from '../../images/robinhoop-logo-light.png';
7 | import {useHistory} from 'react-router-dom'
8 |
9 | const NavBar = () => {
10 | const user = useSelector(state => state.session.user);
11 | const history = useHistory()
12 |
13 | const demoTest = () => {
14 | return user.id === 1 ? false : true
15 | }
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
history.push(`/`)}
25 | >
26 | {!user ?
27 | <>> :
28 | <>
29 |
30 | Portfolio
31 |
32 |
33 | Buy
34 |
35 |
36 | Watchlist
37 |
38 | >
39 | }
40 |
41 |
42 |
43 | {!user ?
44 | <>
45 |
46 | Login
47 |
48 |
49 | Join Now
50 |
51 | >
52 | : <>
53 | {demoTest() &&
54 | Account
55 | }
56 |
57 | >
58 | }
59 |
60 |
61 | >
62 | );
63 | }
64 |
65 | export default NavBar;
66 |
--------------------------------------------------------------------------------
/react-app/src/components/TeamsList/teams-list.css:
--------------------------------------------------------------------------------
1 | .BrowseTeamHeader {
2 | margin-bottom: 20px;
3 | }
4 |
5 | .buy-page {
6 | padding-bottom: 15px;
7 | }
8 |
9 | .team-header {
10 | display: flex;
11 | justify-content: center;
12 | }
13 |
14 | .team-container:nth-child(odd) {
15 | background: rgba(250, 250, 250, .3);
16 | }
17 |
18 | .team-container:nth-child(even) {
19 | background: rgba(250, 250, 250, .15);
20 | }
21 |
22 | .team-container.head {
23 | background: #000;
24 | border-bottom: 2px solid #FFAA01;
25 | border-radius: 0px;
26 | }
27 |
28 | .team-container:nth-child(odd):hover,
29 | .team-container:nth-child(even):hover {
30 | background: rgba(255, 170, 1, .75)
31 | }
32 |
33 | .team-container.head:hover {
34 | background: #000;
35 | }
36 |
37 | .team-container {
38 | display: grid;
39 | grid-template-columns: 1fr 3fr 1fr 2fr 1fr 2fr 1fr;
40 | border-radius: 35px;
41 | margin: 5px 10%;
42 | color: #fff;
43 | }
44 |
45 | .team-container div {
46 | padding: 0px 10px;
47 | }
48 |
49 | .team-details {
50 | display: flex;
51 | align-items: center;
52 | cursor: pointer;
53 | }
54 |
55 | .team-details {
56 | color: #fff;
57 | text-decoration: none;
58 | font-family: 'Roboto', sans-serif;
59 | }
60 |
61 | .team-details :hover {
62 | transition: .5s;
63 | color: #000;
64 | }
65 |
66 | .team-icon {
67 | display: flex;
68 | justify-content: center;
69 | align-items: center;
70 | }
71 |
72 | .team-icon img {
73 | height: 40px;
74 | width: 40px;
75 | }
76 |
77 | @media screen and (max-width: 1050px) {
78 | .team-conf,
79 | .team-shares,
80 | .team-price {
81 | display: none;
82 | }
83 |
84 | .team-container {
85 | grid-template-columns: 1fr 3fr 1fr 1fr;
86 | }
87 | }
88 |
89 | @media screen and (max-width: 600px) {
90 | .team-details-header,
91 | .team-abbr {
92 | display: none;
93 | }
94 |
95 | .team-container {
96 | border-radius: 0px;
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/react-app/src/store/watchlist.js:
--------------------------------------------------------------------------------
1 | // Constants
2 | const GET_LIST = "watchlist/GET_LIST"
3 | const ADD_TO = "watchlist/ADD_TO"
4 | const REMOVE_FROM = "watchlist/REMOVE_FROM"
5 |
6 | // Action Creators
7 | const getList = (list) => ({
8 | type: GET_LIST,
9 | payload: list
10 | })
11 |
12 | const addTo = (team) => ({
13 | type: ADD_TO,
14 | payload: team
15 | })
16 |
17 | const removeFrom = (team) => ({
18 | type: REMOVE_FROM,
19 | payload: team
20 | })
21 |
22 | //Thunks
23 | export const getUserList = (userId) => async (dispatch) => {
24 | const response = await fetch(`/api/watchlist/${userId}`, {
25 | headers: {
26 | 'Content-Type': 'application/json'
27 | }
28 | });
29 | const list = await response.json()
30 | dispatch(getList(list))
31 | return response
32 | }
33 |
34 |
35 | export const addToWatchlist = (userId, teamId) => async (dispatch) => {
36 |
37 |
38 | const response = await fetch(`/api/watchlist/add/${userId}/${teamId}`, {
39 | method: 'POST',
40 | headers: {
41 | 'Content-Type': 'application/json'
42 | },
43 | body: JSON.stringify({
44 | team_id: teamId,
45 | user_id: userId
46 | })
47 | })
48 | const teamx = response.json()
49 | dispatch(addTo(teamx))
50 | return response
51 | }
52 |
53 |
54 | export const removeFromWatchlist = (userId, teamId) => async (dispatch) => {
55 | const response = await fetch(`/api/watchlist/delete/${userId}/${teamId}`, {
56 | method: 'DELETE',
57 | headers: {
58 | 'Content-Type': 'application/json'
59 | },
60 | body: JSON.stringify({
61 | team_id: teamId,
62 | user_id: userId
63 | })
64 | })
65 | const team = await response.json()
66 | dispatch(removeFrom(team))
67 | return response
68 | }
69 |
70 |
71 | // Reducer
72 |
73 | export default function reducer(state={}, action) {
74 | let newState;
75 | switch (action.type) {
76 | case GET_LIST:
77 | newState = {...state, ...action.payload}
78 | return newState
79 | case ADD_TO:
80 | return {...state, ...action.payload}
81 | case REMOVE_FROM:
82 | newState = {...action.payload}
83 | return newState
84 | default:
85 | return state;
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/react-app/src/components/Home/LoggedOutHome.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { login } from "../../store/session";
4 |
5 | import graph from "../../images/graph.png";
6 | import "./home.css"
7 |
8 | const LoggedOutHome = () => {
9 | const dispatch = useDispatch();
10 | // const [errors, setErrors] = useState([]);
11 |
12 | const onDemoLogin = async (e) => {
13 | e.preventDefault();
14 | await dispatch(login("demo@aa.io", "password"));
15 | // if (data.errors) {
16 | // setErrors(data.errors);
17 | // }
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
Robinhoop Investing
27 |
Robinhoop is a simulated clone of the Robinhood Stock Trading app, but focused on Basketball. With Robinhoop, users can invest in their favorite basketball teams (or the ones that are actually winning games). Robinhoop users buy, sell, and keep tabs on NBA basketball teams to see which ones they want to buy stock in, or sell their stock in.
28 |
Join the Game
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Run the Simulation
42 |
Checkout the "Start Simluation" option in the logged in navigation to see how stock prices update.
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | export default LoggedOutHome
52 |
--------------------------------------------------------------------------------
/react-app/src/store/buy.js:
--------------------------------------------------------------------------------
1 | const LOAD_SHARES='buy/LOAD_SHARES'
2 | const BUY_TEAM = 'buy/BUY_TEAM'
3 | const SELL_TEAM= 'buy/SELL_TEAM'
4 |
5 | const loadIt = (purchase) => ({
6 | type: LOAD_SHARES,
7 | payload: purchase
8 | })
9 |
10 | const buyIt = (purchase) => ({
11 | type: BUY_TEAM,
12 | payload: purchase
13 | })
14 |
15 | const sellIt = (id) => ({
16 | type:SELL_TEAM,
17 | payload: id
18 | })
19 |
20 | export const loadShares = (id) => async (dispatch) => {
21 | const response = await fetch (`/api/buy/${id}`)
22 |
23 | if (response.ok) {
24 |
25 | const load = await response.json()
26 | dispatch(loadIt(load))
27 | return load
28 | }
29 |
30 | }
31 |
32 |
33 | export const buyShares = (data) => async (dispatch) => {
34 | const {shares, stockId, userId} = data
35 |
36 | const response = await fetch ('/api/buy', {
37 | method: 'POST',
38 | headers: { 'Content-Type': 'application/json'},
39 | body: JSON.stringify({
40 | shares,
41 | team_id: stockId,
42 | user_id: userId
43 | })
44 | })
45 |
46 | if (response.ok) {
47 |
48 | const buy = await response.json()
49 | dispatch(buyIt(buy))
50 | return buy
51 | }
52 | }
53 |
54 | export const sellShares = (id) => async (dispatch) => {
55 | const response = await fetch(`/api/buy/${id}`, {
56 | method: 'DELETE',
57 | })
58 |
59 | if (response.ok) {
60 | dispatch(sellIt(id))
61 | }
62 | }
63 |
64 |
65 | const initialState = {}
66 |
67 | const buyReducer = (state = initialState, action) => {
68 | let newState = {}
69 | switch (action.type) {
70 | case LOAD_SHARES: {
71 | action.payload.forEach(buy => {
72 | newState.currentBuy[buy.id] = buy
73 | });
74 | // newState = {...state}
75 | // newState.currentBuy = action.payload
76 |
77 | return{...newState, ...state}
78 | }
79 |
80 | case BUY_TEAM: {
81 | newState = {...state}
82 | newState.currentBuy = {...newState.currentBuy}
83 | newState.currentBuy[action.payload.id] = action.payload
84 | return newState
85 | }
86 |
87 | case SELL_TEAM: {
88 | newState = {...state}
89 | delete newState.currentBuy[action.payload]
90 | newState.currentBuy = {...newState.currentBuy}
91 | return newState
92 | }
93 | default:
94 | return state;
95 |
96 | }
97 | }
98 |
99 | export default buyReducer
--------------------------------------------------------------------------------
/react-app/src/components/NavBar/mobile.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import LogoutButton from '../auth/LogoutButton';
5 | import SearchBar from "../Search/index";
6 | import logo from '../../images/robinhoop-logo-light.png';
7 | import {useHistory} from 'react-router-dom'
8 | import './mobile.css'
9 |
10 |
11 | const Mobile = () => {
12 | const user = useSelector(state => state.session.user);
13 | const history = useHistory()
14 | const [isOpen, setIsOpen] = useState(false)
15 |
16 | const demoTest = () => {
17 | return user.id === 1 ? false : true
18 | }
19 |
20 | const toggleMobile = () => {
21 | setIsOpen(!isOpen)
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
history.push(`/`)}
32 | >
33 |
34 |
35 |
36 |
37 |
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | export default Mobile;
70 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, render_template, request, session, redirect
3 | from flask_cors import CORS
4 | from flask_migrate import Migrate
5 | from flask_wtf.csrf import CSRFProtect, generate_csrf
6 | from flask_login import LoginManager
7 |
8 | from .models import db, User
9 | from .api.user_routes import user_routes
10 | from .api.auth_routes import auth_routes
11 | from .api.team_routes import team_routes
12 | from .api.watchlist_routes import watchlist_routes
13 | from .api.buy_routes import buy_routes
14 |
15 | from .seeds import seed_commands
16 |
17 | from .config import Config
18 |
19 | app = Flask(__name__)
20 |
21 | # Setup login manager
22 | login = LoginManager(app)
23 | login.login_view = 'auth.unauthorized'
24 |
25 |
26 | @login.user_loader
27 | def load_user(id):
28 | return User.query.get(int(id))
29 |
30 |
31 | # Tell flask about our seed commands
32 | app.cli.add_command(seed_commands)
33 |
34 | app.config.from_object(Config)
35 | app.register_blueprint(user_routes, url_prefix='/api/users')
36 | app.register_blueprint(auth_routes, url_prefix='/api/auth')
37 | app.register_blueprint(team_routes, url_prefix="/api/teams")
38 | app.register_blueprint(watchlist_routes, url_prefix="/api/watchlist")
39 | app.register_blueprint(buy_routes, url_prefix="/api/buy")
40 | db.init_app(app)
41 | Migrate(app, db)
42 |
43 | # Application Security
44 | CORS(app)
45 |
46 | # Since we are deploying with Docker and Flask,
47 | # we won't be using a buildpack when we deploy to Heroku.
48 | # Therefore, we need to make sure that in production any
49 | # request made over http is redirected to https.
50 | # Well.........
51 |
52 |
53 | @app.before_request
54 | def https_redirect():
55 | if os.environ.get('FLASK_ENV') == 'production':
56 | if request.headers.get('X-Forwarded-Proto') == 'http':
57 | url = request.url.replace('http://', 'https://', 1)
58 | code = 301
59 | return redirect(url, code=code)
60 |
61 |
62 | @app.after_request
63 | def inject_csrf_token(response):
64 | response.set_cookie('csrf_token',
65 | generate_csrf(),
66 | secure=True if os.environ.get(
67 | 'FLASK_ENV') == 'production' else False,
68 | samesite='Strict' if os.environ.get(
69 | 'FLASK_ENV') == 'production' else None,
70 | httponly=True)
71 | return response
72 |
73 |
74 | @app.route('/', defaults={'path': ''})
75 | @app.route('/')
76 | def react_root(path):
77 | print("path", path)
78 | if path == 'favicon.ico':
79 | return app.send_static_file('favicon.ico')
80 | return app.send_static_file('index.html')
81 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Redirect } from "react-router-dom";
4 | import { login } from "../../store/session";
5 | import "./forms.css";
6 |
7 | const LoginForm = () => {
8 | const dispatch = useDispatch();
9 | const user = useSelector(state => state.session.user)
10 | const [errors, setErrors] = useState([]);
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 |
14 | const onLogin = async (e) => {
15 | e.preventDefault();
16 | const data = await dispatch(login(email, password));
17 | if (data.errors) {
18 | setErrors(data.errors);
19 | }
20 | };
21 |
22 | const onDemoLogin = async (e) => {
23 | e.preventDefault();
24 | const data = await dispatch(login("demo@aa.io", "password"));
25 | if (data.errors) {
26 | setErrors(data.errors);
27 | }
28 | }
29 |
30 | const updateEmail = (e) => {
31 | setEmail(e.target.value);
32 | };
33 |
34 | const updatePassword = (e) => {
35 | setPassword(e.target.value);
36 | };
37 |
38 | if (user) {
39 | return ;
40 | }
41 |
42 | return (
43 |
86 | );
87 | };
88 |
89 | export default LoginForm;
90 |
--------------------------------------------------------------------------------
/react-app/src/components/TeamsList/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import {useHistory} from 'react-router-dom'
4 | import placeholder from "../../images/robinhoop-background-ball.jpg";
5 | import "./teams-list.css"
6 |
7 | function TeamsList() {
8 | const teams = useSelector((state) => state.stocks.allStocks)
9 | const history = useHistory()
10 |
11 | const formatter = new Intl.NumberFormat('en-US', {
12 | style: 'currency',
13 | currency: 'USD'
14 | });
15 |
16 | return (
17 |
18 |
Browse Teams
19 |
20 |
23 |
26 |
29 |
32 |
35 |
38 |
41 |
42 | {Object.values(teams)?.map((team) => {
43 | return (
44 |
45 |
46 |
47 |
48 |
51 |
52 |
{team.short_name}
53 |
54 |
55 |
{team.conference}
56 |
57 |
58 |
{new Intl.NumberFormat().format(team.shares)}
59 |
60 |
61 |
{formatter.format(team.price)}
62 |
63 |
history.push(`/stock/${team.id}`)} className="team-details">
64 | Details
65 |
66 |
67 | )
68 | })}
69 |
70 | )
71 | }
72 |
73 | export default TeamsList;
74 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/forms.css:
--------------------------------------------------------------------------------
1 | html, body, #root {
2 | height: 100%;
3 | }
4 |
5 | .container {
6 | height: 100%;
7 | background: #000;
8 | display: flex;
9 | flex-wrap: wrap;
10 | }
11 |
12 | #error-list > div {
13 | background: rgba(353, 98, 41, .3) ;
14 | padding: 5px;
15 | width: 100%;
16 | font-family: 'Roboto', sans-serif;
17 | color: #fff;
18 | }
19 |
20 | .left-side {
21 | background-position: center center;
22 | background-size: cover;
23 | background-repeat: no-repeat;
24 | width: 50%;
25 | }
26 |
27 | .left-side.login {
28 | background-image: url("../../images/robinhoop-background-basketballgame.jpg");
29 | }
30 |
31 | .left-side.join {
32 | background-image: url("../../images/robinhoop-background-basketballcourt.jpg");
33 | }
34 |
35 | .left-side.change {
36 | background-image: url("../../images/robinhoop-background-ball.jpg");
37 | }
38 |
39 | .right-side {
40 | width: 50%;
41 | display: flex;
42 | justify-content: center;
43 | align-items: center;
44 | }
45 |
46 | form .greeting {
47 | color: #fff;
48 | text-align: center;
49 | margin: 0 auto;
50 | font-family: 'Roboto', sans-serif;
51 | }
52 |
53 | form .greeting a {
54 | color: #FFAA01;
55 | text-decoration: none;
56 | }
57 |
58 | form .greeting h1 {
59 | font-family: 'Josefin Sans', sans-serif;
60 | }
61 |
62 | input {
63 | padding: 10px;
64 | border: 2px solid #fff;
65 | border-radius: 5px;
66 | margin-bottom: 10px;
67 | width: 100%;
68 | }
69 |
70 | input:hover {
71 | transition: .5s;
72 | border: 2px solid #FFAA01;
73 | }
74 |
75 | label {
76 | display: none;
77 | }
78 |
79 | form > button {
80 | padding: 15px;
81 | border: 0px;
82 | border-radius: 35px;
83 | text-decoration: none;
84 | font-size: 18px;
85 | font-family: 'Josefin Sans', sans-serif;
86 | background: #097A00;
87 | color: #fff;
88 | }
89 |
90 | form > button:hover {
91 | transition: .5s;
92 | color: #000;
93 | background: #FFAA01;
94 | }
95 |
96 | form.demo {
97 | position: relative;
98 | margin-top: 5px;
99 | }
100 |
101 | .edit-wrapper {
102 | /* position: relative; */
103 | display: flex;
104 | justify-content: center;
105 | /* padding: 10% 0px; */
106 | }
107 |
108 | #errors p {
109 | width: 100%;
110 | padding: 12px;
111 | margin: 0 0 10px 0;
112 | background:rgba(353, 98, 41, .3);
113 | }
114 |
115 | @media screen and (max-width: 767px) {
116 | .left-side {
117 | width: 100%;
118 | }
119 |
120 | .right-side {
121 | width: 100%;
122 | justify-content: center;
123 | }
124 |
125 | form > div {
126 | max-width: 95%;
127 | }
128 | }
129 |
130 | @media screen and (max-width: 320px) {
131 |
132 | .right-side {
133 | max-width: 80%;
134 | margin: 0 auto;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/app/api/auth_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, session, request
2 | from app.models import User, db
3 | from app.forms import LoginForm
4 | from app.forms import SignUpForm
5 | from flask_login import current_user, login_user, logout_user, login_required
6 |
7 | auth_routes = Blueprint('auth', __name__)
8 |
9 |
10 | def validation_errors_to_error_messages(validation_errors):
11 | """
12 | Simple function that turns the WTForms validation errors into a simple list
13 | """
14 | errorMessages = []
15 | for field in validation_errors:
16 | for error in validation_errors[field]:
17 | errorMessages.append(f"{field} : {error}")
18 | return errorMessages
19 |
20 |
21 | @auth_routes.route('/')
22 | def authenticate():
23 | """
24 | Authenticates a user.
25 | """
26 | if current_user.is_authenticated:
27 | return current_user.to_dict()
28 | return {'errors': ['Unauthorized']}
29 |
30 |
31 | @auth_routes.route('/login', methods=['POST'])
32 | def login():
33 | """
34 | Logs a user in
35 | """
36 | form = LoginForm()
37 | print(request.get_json())
38 | # Get the csrf_token from the request cookie and put it into the
39 | # form manually to validate_on_submit can be used
40 | form['csrf_token'].data = request.cookies['csrf_token']
41 | if form.validate_on_submit():
42 | # Add the user to the session, we are logged in!
43 | user = User.query.filter(User.email == form.data['email']).first()
44 | login_user(user)
45 | return user.to_dict()
46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
47 |
48 |
49 | @auth_routes.route('/demologin', methods=["POST"])
50 | def demologin():
51 | user = User.query.filter(User.id == 1)
52 | login_user(user)
53 | return user.to_dict()
54 |
55 |
56 | @auth_routes.route('/logout')
57 | def logout():
58 | """
59 | Logs a user out
60 | """
61 | logout_user()
62 | return {'message': 'User logged out'}
63 |
64 |
65 | @auth_routes.route('/signup', methods=['POST'])
66 | def sign_up():
67 | """
68 | Creates a new user and logs them in
69 | """
70 | form = SignUpForm()
71 | form['csrf_token'].data = request.cookies['csrf_token']
72 | if User.query.filter(User.email == form.data['email']).first():
73 | return {'errors': ['Email already in use']}, 409
74 | if form.validate_on_submit():
75 | user = User(
76 | username=form.data['username'],
77 | email=form.data['email'],
78 | password=form.data['password']
79 | )
80 | db.session.add(user)
81 | db.session.commit()
82 | login_user(user)
83 | return user.to_dict()
84 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
85 |
86 |
87 | @auth_routes.route('/unauthorized')
88 | def unauthorized():
89 | """
90 | Returns unauthorized JSON when flask-login authentication fails
91 | """
92 | return {'errors': ['Unauthorized']}, 401
93 |
--------------------------------------------------------------------------------
/migrations/versions/20210623_154841_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 4a10df1251af
4 | Revises: ffdc0a98111c
5 | Create Date: 2021-06-23 15:48:41.627289
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '4a10df1251af'
14 | down_revision = 'ffdc0a98111c'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table(
22 | 'teams',
23 | sa.Column('id', sa.Integer(), nullable=False),
24 | sa.Column('name', sa.String(), nullable=False),
25 | sa.Column('short_name', sa.String(), nullable=False),
26 | sa.Column('conference', sa.String(), nullable=False),
27 | sa.Column('shares', sa.Integer(), nullable=False),
28 | sa.Column('price', sa.Float(), nullable=False),
29 | sa.PrimaryKeyConstraint('id'),
30 | sa.UniqueConstraint('short_name')
31 | )
32 | op.create_table(
33 | 'events',
34 | sa.Column('id', sa.Integer(), nullable=False),
35 | sa.Column('date', sa.Date(), nullable=False),
36 | sa.Column('winner_id', sa.Integer(), nullable=False),
37 | sa.Column('winner_score', sa.Integer(), nullable=False),
38 | sa.Column('loser_id', sa.Integer(), nullable=False),
39 | sa.Column('loser_score', sa.Integer(), nullable=False),
40 | sa.ForeignKeyConstraint(['loser_id'], ['teams.id'], ),
41 | sa.ForeignKeyConstraint(['winner_id'], ['teams.id'], ),
42 | sa.PrimaryKeyConstraint('id')
43 | )
44 | op.create_table(
45 | 'history',
46 | sa.Column('id', sa.Integer(), nullable=False),
47 | sa.Column('team_id', sa.Integer(), nullable=False),
48 | sa.Column('price', sa.Float(), nullable=False),
49 | sa.Column('date', sa.Date(), nullable=False),
50 | sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
51 | sa.PrimaryKeyConstraint('id')
52 | )
53 | op.create_table(
54 | 'usershare',
55 | sa.Column('id', sa.Integer(), nullable=False),
56 | sa.Column('shares', sa.Integer(), nullable=True),
57 | sa.Column('user_id', sa.Integer(), nullable=False),
58 | sa.Column('team_id', sa.Integer(), nullable=False),
59 | sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
60 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
61 | sa.PrimaryKeyConstraint('id')
62 | )
63 | op.create_table(
64 | 'watchlists',
65 | sa.Column('id', sa.Integer(), nullable=False),
66 | sa.Column('team_id', sa.Integer(), nullable=False),
67 | sa.Column('user_id', sa.Integer(), nullable=False),
68 | sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
69 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
70 | sa.PrimaryKeyConstraint('id')
71 | )
72 | # ### end Alembic commands ###
73 |
74 |
75 | def downgrade():
76 | # ### commands auto generated by Alembic - please adjust! ###
77 | op.drop_table('watchlists')
78 | op.drop_table('usershare')
79 | op.drop_table('history')
80 | op.drop_table('events')
81 | op.drop_table('teams')
82 | # ### end Alembic commands ###
83 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 | from flask import current_app
11 |
12 | # this is the Alembic Config object, which provides
13 | # access to the values within the .ini file in use.
14 | config = context.config
15 |
16 | # Interpret the config file for Python logging.
17 | # This line sets up loggers basically.
18 | fileConfig(config.config_file_name)
19 | logger = logging.getLogger('alembic.env')
20 |
21 | # add your model's MetaData object here
22 | # for 'autogenerate' support
23 | # from myapp import mymodel
24 | # target_metadata = mymodel.Base.metadata
25 | config.set_main_option(
26 | 'sqlalchemy.url',
27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
28 | target_metadata = current_app.extensions['migrate'].db.metadata
29 |
30 | # other values from the config, defined by the needs of env.py,
31 | # can be acquired:
32 | # my_important_option = config.get_main_option("my_important_option")
33 | # ... etc.
34 |
35 |
36 | def run_migrations_offline():
37 | """Run migrations in 'offline' mode.
38 |
39 | This configures the context with just a URL
40 | and not an Engine, though an Engine is acceptable
41 | here as well. By skipping the Engine creation
42 | we don't even need a DBAPI to be available.
43 |
44 | Calls to context.execute() here emit the given string to the
45 | script output.
46 |
47 | """
48 | url = config.get_main_option("sqlalchemy.url")
49 | context.configure(
50 | url=url, target_metadata=target_metadata, literal_binds=True
51 | )
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online():
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 |
65 | # this callback is used to prevent an auto-migration from being generated
66 | # when there are no changes to the schema
67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
68 | def process_revision_directives(context, revision, directives):
69 | if getattr(config.cmd_opts, 'autogenerate', False):
70 | script = directives[0]
71 | if script.upgrade_ops.is_empty():
72 | directives[:] = []
73 | logger.info('No changes in schema detected.')
74 |
75 | connectable = engine_from_config(
76 | config.get_section(config.config_ini_section),
77 | prefix='sqlalchemy.',
78 | poolclass=pool.NullPool,
79 | )
80 |
81 | with connectable.connect() as connection:
82 | context.configure(
83 | connection=connection,
84 | target_metadata=target_metadata,
85 | process_revision_directives=process_revision_directives,
86 | **current_app.extensions['migrate'].configure_args
87 | )
88 |
89 | with context.begin_transaction():
90 | context.run_migrations()
91 |
92 |
93 | if context.is_offline_mode():
94 | run_migrations_offline()
95 | else:
96 | run_migrations_online()
97 |
--------------------------------------------------------------------------------
/react-app/src/components/Search/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import placeholder from "../../images/robinhoop-background-ball.jpg";
3 | import { useHistory } from "react-router-dom";
4 | import './search.css'
5 |
6 | const SearchBar = () => {
7 |
8 | const history = useHistory();
9 | const [teams, setTeams] = useState([]);
10 | const [hidden, setHidden] = useState(true)
11 |
12 | useEffect(() => {
13 | async function fetchData() {
14 | const response = await fetch("/api/teams/");
15 | const responseData = await response.json();
16 | setTeams(responseData.teams);
17 | }
18 | fetchData();
19 | }, []);
20 |
21 | const filterTeams = (teams, query) => {
22 | if (!query) {
23 | return [];
24 | }
25 |
26 | return teams.filter((team) => {
27 | const teamName = team.name.toLowerCase();
28 | const teamAbbr = team.short_name.toLowerCase();
29 | const teamConf = team.conference.toLowerCase();
30 | return teamName.includes(query.toLowerCase()) || teamAbbr.includes(query.toLowerCase()) || teamConf.includes(query.toLowerCase());
31 | })
32 | }
33 |
34 |
35 | const { search } = window.location;
36 | const query = new URLSearchParams(search).get('s');
37 | const [searchQuery, setSearchQuery] = useState(query || "");
38 | const filteredTeams = filterTeams(teams, searchQuery);
39 |
40 | const onSubmit = e => {
41 | history.push(`?s=${searchQuery}`)
42 | e.preventDefault()
43 | };
44 |
45 |
46 | return (
47 |
81 | )
82 | }
83 |
84 | export default SearchBar;
85 |
--------------------------------------------------------------------------------
/react-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom";
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import LoginForm from "./components/auth/LoginForm";
5 | import SignUpForm from "./components/auth/SignUpForm";
6 | import EditUserForm from "./components/auth/EditUserForm";
7 | import NavBar from "./components/NavBar/index";
8 | import Mobile from './components/NavBar/mobile'
9 | import Footer from "./components/Footer/index";
10 | import Home from "./components/Home/index"
11 | import Watchlist from "./components/Watchlist";
12 | import UsersList from "./components/User/UsersList";
13 | import StockPage from "./components/StockPage";
14 | import TeamsList from "./components/TeamsList/index";
15 | import NotFound from "./components/404/notFound.js";
16 | import { authenticate } from "./store/session";
17 | import { getAllStocks } from "./store/stocks";
18 | import { getUserList } from "./store/watchlist";
19 |
20 | function App() {
21 | const dispatch = useDispatch();
22 | const [loaded, setLoaded] = useState(false);
23 | const [isMobile, setIsMobile] = useState(false)
24 | let listener = window.innerWidth;
25 |
26 | useEffect(() => {
27 | (async () => {
28 | const userId = await dispatch(authenticate());
29 | await dispatch(getAllStocks())
30 | if (userId) await dispatch(getUserList(userId))
31 | setLoaded(true);
32 | })();
33 |
34 | listener <= 870 ? setIsMobile(true) : setIsMobile(false);
35 |
36 | }, [dispatch]);
37 |
38 |
39 | const checkForMobile = () => {
40 | if (listener <= 870) {
41 | setIsMobile(true)
42 | } else {
43 | setIsMobile(false)
44 | }
45 | }
46 |
47 | const currentUser = useSelector((state) => state.session.user)
48 |
49 | if (!loaded) {
50 | return null;
51 | }
52 |
53 | return (
54 |
55 | {/* { isMobile ? : } */}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | {currentUser ?
68 |
69 | :
70 | }
71 | {currentUser ?
72 |
73 | :
74 | }
75 | {currentUser ?
76 |
77 | :
78 | }
79 | {currentUser ?
80 |
81 | :
82 | }
83 | {currentUser ?
84 |
85 | :
86 | }
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
99 | export default App;
100 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Redirect } from 'react-router-dom';
4 | import { signUp } from '../../store/session';
5 | import "./forms.css";
6 |
7 | const SignUpForm = () => {
8 | const dispatch = useDispatch();
9 | const user = useSelector(state => state.session.user);
10 | const [username, setUsername] = useState("");
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 | const [repeatPassword, setRepeatPassword] = useState("");
14 | const [errors, setErrors] = useState([])
15 |
16 | const onSignUp = async (e) => {
17 | e.preventDefault();
18 | if (password === repeatPassword) {
19 | const newUser = await dispatch(signUp(username, email, password));
20 | if (newUser.length > 0) {
21 | setErrors(newUser)
22 | }
23 | } else {
24 | setErrors(['Passwords do not match.'])
25 | }
26 | };
27 |
28 | const updateUsername = (e) => {
29 | setUsername(e.target.value);
30 | };
31 |
32 | const updateEmail = (e) => {
33 | setEmail(e.target.value);
34 | };
35 |
36 | const updatePassword = (e) => {
37 | setPassword(e.target.value);
38 | };
39 |
40 | const updateRepeatPassword = (e) => {
41 | setRepeatPassword(e.target.value);
42 | };
43 |
44 | if (user) {
45 | return ;
46 | }
47 |
48 | return (
49 |
108 | );
109 | };
110 |
111 | export default SignUpForm;
112 |
--------------------------------------------------------------------------------
/react-app/src/store/stocks.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const ALL_STOCKS = 'stocks/ALL_STOCKS'
4 | const USER_SHARES = 'stocks/USER_SHARES'
5 | const ADD_HISTORY = 'stocks/ADD_HISTORY'
6 |
7 |
8 | const allStocks = (stocks) => ({
9 | type: ALL_STOCKS,
10 | payload: stocks
11 | })
12 |
13 | const userShares = (shares) => ({
14 | type: USER_SHARES,
15 | payload: shares
16 | })
17 |
18 | const addHistory = (history) => ({
19 | type: ADD_HISTORY,
20 | payload: history
21 | })
22 |
23 |
24 | export const getAllStocks = () => async (dispatch) => {
25 | const response = await fetch('/api/teams/')
26 | const data = await response.json()
27 | dispatch(allStocks(data))
28 | return data
29 | }
30 |
31 | export const getUserShares = (userId, stockId) => async (dispatch) => {
32 | const response = await fetch(`/api/teams/userShares/${userId}/${stockId}`)
33 | const data = await response.json()
34 | dispatch(userShares(data))
35 | return data
36 | }
37 |
38 | export const makeStockHistory = (stockId) => async (dispatch) => {
39 | const response = await fetch(`/api/teams/${stockId}`)
40 | const stockData = await response.json()
41 | const currentDate = new Date()
42 | dispatch(addHistory({
43 | team_id: stockId,
44 | price: stockData.price,
45 | date: `${currentDate.getMonth()} / ${currentDate.getDate()}`
46 | }))
47 | }
48 |
49 | export const updateStock = (stockId, diff, win) => async (dispatch) => {
50 | const response = await fetch(`/api/teams/${stockId}`)
51 | const stockData = await response.json()
52 | const differ = (Math.random() * 5)
53 | const comb = diff * differ // * 100 round /100
54 | const fixed = comb.toFixed(2)
55 | const stockChange = parseFloat(fixed)
56 |
57 | const stockAdd = parseFloat(stockData.price.toFixed(2))
58 |
59 | const newPrice = win ? parseFloat((stockAdd + stockChange).toFixed(2)) : parseFloat((stockAdd - stockChange).toFixed(2))
60 | const res = await fetch(`/api/teams/editPrice/${stockId}`, {
61 | method: 'PUT',
62 | headers: {
63 | 'Content-Type': 'application/json'
64 | },
65 | body: JSON.stringify({
66 | price: newPrice
67 | })
68 | })
69 | const data = await res.json()
70 | dispatch(allStocks(data))
71 | return data
72 | }
73 |
74 |
75 |
76 | const reducer = (state = { allStocks: {}, userShares: null, history: {} }, action) => {
77 | let newState;
78 |
79 | switch (action.type) {
80 | case ALL_STOCKS:
81 | newState = { allStocks: {}, userShares: state.userShares, history: state.history }
82 | action.payload.teams.forEach(stock => {
83 | newState.allStocks[stock.id] = stock
84 | })
85 | return newState
86 | case USER_SHARES:
87 | newState = { allStocks: state.allStocks, userShares: null, history: state.history }
88 | newState.userShares = action.payload
89 | return newState
90 | case ADD_HISTORY:
91 | newState = { allStocks: state.allStocks, userShares: state.userShares, history: state.history }
92 | const id = action.payload.team_id
93 | if (newState.history[id]) {
94 | newState.history[id].push(action.payload)
95 | } else {
96 | newState.history[id] = [action.payload]
97 | }
98 | return newState
99 | default:
100 | return state
101 | }
102 | }
103 |
104 | export default reducer
105 |
--------------------------------------------------------------------------------
/react-app/src/components/StockPage/stockpage.css:
--------------------------------------------------------------------------------
1 | .container.stock {
2 | height: auto !important;
3 | }
4 |
5 | .parent-container {
6 | display: flex;
7 | flex-wrap: wrap;
8 | }
9 |
10 | canvas {
11 | height: 50%;
12 | max-width: 50%;
13 | }
14 |
15 | .parent__cont {
16 | color: #fff;
17 | display: flex;
18 | align-items: center;
19 | flex-direction: row;
20 | width: 50%;
21 | padding-bottom: 25px;
22 | }
23 |
24 | /* .graph__cont {
25 | width: 40%;
26 | height: 60%;
27 | } */
28 |
29 | .info__buy__sell__parent__cont {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | flex-direction: column;
34 | width: 100%;
35 | }
36 |
37 | .info__cont {
38 | display: flex;
39 | justify-content: center;
40 | flex-direction: column;
41 | width: 40%;
42 | margin: 5%;
43 | }
44 |
45 | .info__cont div {
46 | padding: 5px;
47 | }
48 |
49 | .info__stock__name,
50 | .info__stock__conference,
51 | .info__stock__short_name,
52 | .info__stock__shares,
53 | .info__stock__price,
54 | .info__stock__icon {
55 | display: flex;
56 | justify-content: center;
57 | text-align: center;
58 | }
59 |
60 | a.watchlist-link {
61 | padding: 15px;
62 | font-family: 'Josefin Sans', sans-serif;
63 | font-size: 18px;
64 | text-decoration: none;
65 | text-align: center;
66 | color: #fff;
67 | }
68 |
69 | a.watchlist-link:hover {
70 | transition: .5s;
71 | color: #FFAA01;
72 | }
73 |
74 | .info__stock__name {
75 | font-weight: bold;
76 | font-size: 30pt;
77 | font-family: 'Josefin Sans', sans-serif;
78 | }
79 |
80 | .info__stock__short_name {
81 | font-weight: bold;
82 | font-style: italic;
83 | }
84 |
85 | .info__stock__conference,
86 | .info__stock__short_name,
87 | .info__stock__shares {
88 | font-size: 16pt;
89 | font-family: 'Roboto', sans-serif;
90 | }
91 |
92 | .info__stock__shares {
93 | margin-top: 30px;
94 |
95 | }
96 |
97 | .info__stock__price {
98 | font-size: 24pt;
99 | font-family: 'Roboto', sans-serif;
100 | }
101 |
102 | .info__stock__icon img {
103 | height: 100px;
104 | width: 100px;
105 | }
106 |
107 | .buy__sell__cont {
108 | display: flex;
109 | justify-content: space-around;
110 | width: 70%;
111 | }
112 |
113 | .buy__cont,
114 | .sell__cont {
115 | display: flex;
116 | flex-direction: column;
117 | margin: 20px;
118 | }
119 |
120 | .buy__cont div,
121 | .sell__cont div {
122 | padding: 5px;
123 | font-size: 16pt;
124 | font-family: 'Roboto', sans-serif;
125 | font-weight: 500;
126 | }
127 |
128 | .buy__title,
129 | .sell__title {
130 | font-weight: bold;
131 | }
132 |
133 | .buy__quant__input,
134 | .sell__quant__input {
135 | padding: 5px;
136 | margin: 0px;
137 | }
138 |
139 | @media screen and (max-width: 1366px) {
140 | .parent-container {
141 | display: block;
142 | }
143 |
144 | canvas {
145 | height: auto;
146 | max-width: 100%;
147 | }
148 |
149 | .parent__cont {
150 | color: #fff;
151 | display: flex;
152 | align-items: center;
153 | flex-direction: row;
154 | width: 100%;
155 | }
156 |
157 | .buy__cont,
158 | .sell__cont {
159 | display: block;
160 | margin: 20px;
161 | }
162 | }
163 |
164 | @media screen and (max-width: 768px) {
165 | .parent__cont, .buy__sell__cont {
166 | display: block;
167 | }
168 |
169 | .graph__cont {
170 | width: 100%;
171 | height: auto;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/react-app/src/store/session.js:
--------------------------------------------------------------------------------
1 | // Constants
2 | const SET_USER = "session/SET_USER";
3 | const REMOVE_USER = "session/REMOVE_USER";
4 | const EDIT_USER = "session/EDIT_USER"
5 |
6 | // Action Creators
7 | const setUser = (user) => ({
8 | type: SET_USER,
9 | payload: user
10 | });
11 |
12 | const removeUser = () => ({
13 | type: REMOVE_USER,
14 | });
15 |
16 | const editUser = (user) => ({
17 | type: EDIT_USER,
18 | payload: user
19 | });
20 |
21 | //Thunks
22 | export const authenticate = () => async(dispatch) => {
23 | const response = await fetch('/api/auth/',{
24 | headers: {
25 | 'Content-Type': 'application/json'
26 | }
27 | });
28 |
29 | const data = await response.json();
30 |
31 | if (data.errors) {
32 | return;
33 | }
34 |
35 | dispatch(setUser(data))
36 |
37 | return data.id;
38 | }
39 |
40 | export const login = (email, password) => async (dispatch) => {
41 | const response = await fetch('/api/auth/login', {
42 | method: 'POST',
43 | headers: {
44 | 'Content-Type': 'application/json'
45 | },
46 | body: JSON.stringify({
47 | email,
48 | password
49 | })
50 | });
51 |
52 | const data = await response.json();
53 |
54 | if (data.errors) {
55 | return data;
56 | }
57 |
58 | dispatch(setUser(data))
59 | return {}
60 | }
61 |
62 | export const demoLogin = (id) => async (dispatch) => {
63 | const response = await fetch('.api/auth/demologin', {
64 | method: "POST",
65 | headers: {
66 | "Content-Type": "application/json"
67 | },
68 | body: JSON.stringify({
69 | id
70 | })
71 | });
72 |
73 | const data = await response.json();
74 |
75 | if (data.errors) {
76 | return data;
77 | }
78 |
79 | dispatch(setUser(data))
80 | return {}
81 | }
82 |
83 | export const logout = () => async (dispatch) => {
84 | const response = await fetch("/api/auth/logout", {
85 | headers: {
86 | "Content-Type": "application/json",
87 | }
88 | });
89 |
90 | await response.json();
91 | dispatch(removeUser())
92 | };
93 |
94 |
95 | export const signUp = (username, email, password) => async (dispatch) => {
96 | const response = await fetch("/api/auth/signup", {
97 | method: "POST",
98 | headers: {
99 | "Content-Type": "application/json",
100 | },
101 | body: JSON.stringify({
102 | username,
103 | email,
104 | password,
105 | }),
106 | });
107 |
108 | const data = await response.json();
109 |
110 | if (data.errors) {
111 | return data.errors
112 | }
113 |
114 | dispatch(setUser(data))
115 | }
116 |
117 |
118 | export const edit = (username, email, password, repeat_password) => async (dispatch) => {
119 | const response = await fetch("/api/users/edit-account", {
120 | method: "POST",
121 | headers: {
122 | "Content-Type": "application/json",
123 | },
124 | body: JSON.stringify({
125 | username,
126 | email,
127 | password,
128 | repeat_password
129 | }),
130 | });
131 |
132 | const data = await response.json();
133 |
134 | if (data.errors) {
135 | return
136 | }
137 |
138 | dispatch(editUser(data))
139 | }
140 |
141 | // Reducer
142 |
143 | const initialState = {user: null}
144 |
145 | export default function reducer(state=initialState, action) {
146 | switch (action.type) {
147 | case SET_USER:
148 | return {user: action.payload}
149 | case REMOVE_USER:
150 | return {user: null}
151 | case EDIT_USER:
152 | return { user: action.payload }
153 | default:
154 | return state
155 | }
156 | };
157 |
--------------------------------------------------------------------------------
/react-app/src/components/Home/home.css:
--------------------------------------------------------------------------------
1 | /* NOT Logged in User Homepage */
2 | .parent__content {
3 | width: 100%;
4 | color: #fff;
5 | background: #000;
6 | }
7 |
8 | .signup__info__flexcont {
9 | display: flex;
10 | flex-direction: column wrap;
11 | background-image: linear-gradient(to right, rgba(0,0,0,1), rgba(0,0,0,0)), url("../../images/robinhoop-background-court-image.jpg");
12 | background-size: cover;
13 | background-position: center center;
14 | border-bottom: 2px solid #FFAA01;
15 | }
16 |
17 | .solo__info__flexcont {
18 | display: flex;
19 | flex-direction: column wrap;
20 | justify-content: space-around;
21 | align-items: center;
22 | }
23 |
24 | .signup__info__text__cont {
25 | width: 50%;
26 | margin: 8% 4%;
27 | color: #fff;
28 | font-family: 'Roboto', sans-serif
29 | }
30 |
31 | .signup__info__disclaimer__text {
32 | display: block;
33 | font-size: 16px;
34 | }
35 |
36 | a.signup__info__disclaimer__text {
37 | color: #fff;
38 | margin-top: 24px;
39 | text-decoration: none;
40 | }
41 |
42 | a.signup__info__disclaimer__text:hover {
43 | transition: .5s;
44 | color: #FFAA01;
45 | }
46 |
47 | form.demo-link button {
48 | background: rgba(0, 0, 0, 0);
49 | }
50 |
51 | form.demo-link button:hover {
52 | color: #FFAA01;
53 | background: rgba(0, 0, 0, 0);
54 | }
55 |
56 | .material-icons {
57 | font-size: 16px;
58 | }
59 |
60 | .solo__info__img__cont,
61 | .solo__info__text__cont {
62 | padding: 4%;
63 | }
64 |
65 | .solo__info__img__cont img {
66 | width: 100%;
67 | max-width: 500px;
68 | }
69 |
70 | .solo__info__title__text h2 {
71 | font-size: 1.5em;
72 | }
73 |
74 | @media screen and (max-width: 767px) {
75 | .signup__info__flexcont {
76 | background-image: linear-gradient(to right, rgba(0,0,0,1), rgba(0,0,0,.3)), url("../../images/robinhoop-background-court-image.jpg");
77 | }
78 |
79 | .solo__info__flexcont,
80 | .signup__info__text__cont {
81 | width: 100%;
82 | }
83 |
84 | .solo__info__flexcont {
85 | display: block;
86 | }
87 | }
88 |
89 |
90 | /* LOGGED In User Homepage */
91 |
92 | /* .logged-in-homepage {
93 |
94 | } */
95 |
96 | .GreetUser {
97 | /* background: #F0E7E0; */
98 | background-image: linear-gradient(rgba(0,0,0,.7), rgba(0,0,0,.7)), url("../../images/robinhoop-background-basketballgame.jpg");
99 | background-size: cover;
100 | background-position: center center;
101 | padding: 5%;
102 | border-bottom: 2px solid #FFAA01;
103 | color: #fff;
104 | text-align: center;
105 | font-family: 'Josefin Sans', sans-serif;
106 | text-transform: capitalize;
107 | font-size: 32px;
108 | }
109 |
110 | .section-headers {
111 | text-align: center;
112 | font-family: 'Josefin Sans', sans-serif;
113 | color: #fff;
114 | }
115 |
116 | .section-headers h2 {
117 | font-size: 24px;
118 | margin-bottom: 0px;
119 | }
120 |
121 | .section-headers h3 {
122 | font-size: 24px;
123 | margin-bottom: 0px;
124 | }
125 |
126 | .TeamStockCard__carosel {
127 | display: flex;
128 | background: #000;
129 | overflow-x: scroll;
130 | }
131 |
132 | .TeamStockCard__carosel > div {
133 | margin: 2% 3%;
134 | }
135 |
136 | .Article__cont {
137 | display: flex;
138 | flex-wrap: wrap;
139 | justify-content: space-around;
140 | background: #000;
141 | padding-bottom: 2%;
142 | }
143 |
144 | .Article__cont > div {
145 | max-width: 450px;
146 | width: 100%;
147 | }
148 |
149 | button#simulator {
150 | margin: 2% auto;
151 | }
152 |
153 | #simulation-text {
154 | padding-bottom: 4%;
155 | }
156 |
157 | @media screen and (max-width: 902px) {
158 | .Article__cont {
159 | display: block;
160 | }
161 | .Article__cont > div {
162 | max-width: 100%;
163 | padding-top: 1%;
164 | padding-bottom: 1%;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/react-app/src/components/StockPage/StockPageInfo.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { useState } from "react"
3 | import { useSelector, useDispatch } from "react-redux"
4 | import placeholder from "../../images/robinhoop-background-ball.jpg";
5 | import "./stockpage.css"
6 | import * as buyReducer from "../../store/buy"
7 |
8 | const StockPageInfo = ({ stockId }) => {
9 |
10 | const [sellQuant, setSellQuant] = useState(0)
11 | const [shares, setShares] = useState(0)
12 | const user = useSelector(state => state.session.user)
13 | const userId = user.id
14 |
15 |
16 | const stock = useSelector((state) => state.stocks.allStocks[stockId])
17 | const dispatch = useDispatch()
18 | const userShare = useSelector((state) => state.stocks.userShares)
19 |
20 | const buyShare = useSelector(state => state.buy.currentBuy)
21 |
22 |
23 |
24 | const buyHandler = () => {
25 | dispatch(buyReducer.buyShares({shares, stockId, userId}))
26 | window.alert(`Succesfully bought ${shares} shares from the ${stock.name}`)
27 | setShares(0)
28 | }
29 |
30 | const sellHandler = () => {
31 | //dispatch(buyReducer.sellShares({id}))
32 | window.alert(`Succesfully sold ${shares} shares from the ${stock.name}`)
33 | setShares(0)
34 | }
35 |
36 | const formatter = new Intl.NumberFormat('en-US', {
37 | style: 'currency',
38 | currency: 'USD'
39 | });
40 |
41 |
42 | return (
43 |
44 |
45 |
46 |
{stock?.name}
47 |
{stock?.conference}
48 |
{stock?.short_name}
49 |
{new Intl.NumberFormat().format(stock?.shares)} shares
50 |
{formatter.format(stock?.price)}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
Buy {stock?.name}
58 |
setShares(e.target.value)}
62 | value={shares}
63 | min="0"
64 | >
65 |
66 |
{formatter.format(Number((shares * stock?.price).toFixed(2)))}
67 |
buyHandler(e.target.value)}
70 | >Buy Now
71 |
72 |
73 |
74 |
Sell {stock?.name}
75 |
setShares(e.target.value)}
79 | value={shares}
80 | min="0"
81 | >
82 |
83 |
{formatter.format(Number((sellQuant * stock?.price).toFixed(2)))}
84 |
sellHandler()}
87 | >Sell Now
88 |
89 |
90 |
91 |
92 |
93 | )
94 | }
95 |
96 | export default StockPageInfo
97 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/EditUserForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Redirect } from 'react-router-dom'
4 | import { edit } from '../../store/session';
5 | import "./forms.css";
6 |
7 | const EditUserForm = () => {
8 | const dispatch = useDispatch();
9 | const user = useSelector(state => state.session.user);
10 | const [username, setUsername] = useState("")
11 | const [email, setEmail] = useState("")
12 | const [password, setPassword] = useState("");
13 | const [repeatPassword, setRepeatPassword] = useState("");
14 |
15 | const onEdit = async (e) => {
16 | e.preventDefault();
17 | if (password === repeatPassword) {
18 | await dispatch(edit(username, email, password, repeatPassword));
19 | return
20 | }
21 | }
22 |
23 | const updateUsername = (e) => {
24 | setUsername(e.target.value);
25 | };
26 |
27 | const updateEmail = (e) => {
28 | setEmail(e.target.value);
29 | };
30 |
31 | const updatePassword = (e) => {
32 | setPassword(e.target.value);
33 | };
34 |
35 | const updateRepeatPassword = (e) => {
36 | setRepeatPassword(e.target.value);
37 | };
38 |
39 | if (!user) {
40 | return ;
41 | }
42 |
43 | return (
44 |
99 | );
100 | };
101 |
102 | export default EditUserForm
103 |
--------------------------------------------------------------------------------
/react-app/src/components/Home/LoggedInHome.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react"
2 | import { useDispatch, useSelector } from "react-redux"
3 | import TeamStockCard from '../TeamStockCard'
4 | import ArticleDataCard from "../ArticleCard"
5 | import { getUserList } from "../../store/watchlist"
6 | import { getAllStocks, makeStockHistory, updateStock } from "../../store/stocks.js"
7 | import { newArticle } from '../../store/articles.js'
8 |
9 | const LoggedInHome = () => {
10 |
11 | const dispatch = useDispatch()
12 | const stocks = useSelector((state) => state.stocks.allStocks)
13 | const articles = useSelector((state) => state.articles)
14 | const user = useSelector(state => state.session.user)
15 |
16 | const [simulation, setSimulation] = useState(false)
17 | const [simId, setSimId] = useState()
18 | const [buttonText, setButtonText] = useState('Start')
19 | const [hidden, setHidden] = useState(true)
20 |
21 | useEffect(() => {
22 | dispatch(getUserList(user.id))
23 | }, [])
24 |
25 | const triggerSimulation = () => {
26 | if (simulation) {
27 | setSimulation(false)
28 | setButtonText('Start')
29 | setHidden(true)
30 | } else {
31 | setSimulation(true)
32 | setButtonText('Stop')
33 | setHidden(false)
34 | }
35 |
36 |
37 | if (simulation) {
38 | clearInterval(simId)
39 | } else {
40 | setSimId(setInterval(async () => {
41 | let teamIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
42 |
43 | while (teamIds.length) {
44 |
45 | let home = teamIds.splice(Math.floor(Math.random() * teamIds.length), 1)
46 |
47 | let away = teamIds.splice(Math.floor(Math.random() * teamIds.length), 1)
48 |
49 | home.push(Math.round(Math.random() * (125 - 75) + 75))
50 | away.push(Math.round(Math.random() * (125 - 75) + 75))
51 | while (home[1] === away[1]) {
52 | away[1] = (Math.round(Math.random() * (125 - 75) + 75))
53 | }
54 | let diff;
55 |
56 | if (home[1] > away[1]) {
57 | await dispatch(newArticle(home[0], away[0], home[1], away[1]))
58 | diff = home[1] - away[1]
59 | home.push(true)
60 | } else {
61 | await dispatch(newArticle(away[0], home[0], away[1], home[1]))
62 | diff = away[1] - home[1]
63 | home.push(true)
64 | }
65 |
66 | await dispatch(makeStockHistory(home[0]))
67 | await dispatch(makeStockHistory(away[0]))
68 |
69 | await dispatch(updateStock(home[0], diff, home[2]))
70 | await dispatch(updateStock(away[0], diff, away[2]))
71 |
72 | }
73 | dispatch(getAllStocks())
74 | }, 5000))
75 | }
76 | }
77 |
78 | return (
79 | <>
80 |
81 |
{user.username}'s Portfolio
82 |
83 |
Stocks
84 |
85 |
86 | {Object.values(stocks).map(stock => {
87 |
88 | return (
89 |
90 | )
91 | })}
92 |
93 |
94 |
Scores
95 |
triggerSimulation()}
97 | >{buttonText} Simulation
98 |
Running
99 |
100 | {articles.length ?
101 |
102 | {Object.values(articles)?.map((article, i) => {
103 | return (
104 |
105 | )
106 | })}
107 |
108 | :
109 |
Press Button to Start Simulation
110 | (It may take a few seconds.)
111 |
112 | }
113 |
114 | >
115 | )
116 | }
117 |
118 | export default LoggedInHome
119 |
--------------------------------------------------------------------------------
/react-app/src/index.css:
--------------------------------------------------------------------------------
1 | /* Navigation Style */
2 | body {
3 | margin: 0;
4 | }
5 |
6 | nav {
7 | display: grid;
8 | grid-template-columns: 1fr 1fr;
9 | position: fixed;
10 | top: 0;
11 | width: 100%;
12 | background: #000;
13 | }
14 |
15 | .spacer{
16 | min-height: 70px;
17 | }
18 |
19 | nav .left {
20 | display: flex;
21 | flex-direction: row wrap;
22 | justify-content: space-between;
23 | align-items: center;
24 | }
25 |
26 | nav .right {
27 | display: flex;
28 | flex-direction: row wrap;
29 | justify-content: flex-end;
30 | align-items: center;
31 | }
32 |
33 | nav a {
34 | margin: 0 3%;
35 | color: #fff;
36 | text-decoration: none;
37 | font-size: 18px;
38 | font-family: 'Josefin Sans', sans-serif;
39 | }
40 |
41 | nav a:hover {
42 | transition: .5s;
43 | color: #FFAA01;
44 | }
45 |
46 | nav .active {
47 | color: #FFAA01;
48 | }
49 |
50 | button {
51 | cursor: pointer;
52 | }
53 |
54 | #logout {
55 | border: 0px;
56 | margin: 0 5% 0 3%;
57 | color: #fff;
58 | font-size: 18px;
59 | font-family: 'Josefin Sans', sans-serif;
60 | }
61 |
62 | #sign-up, #logout {
63 | background: #097A00;
64 | padding: 15px;
65 | border-radius: 35px;
66 | }
67 |
68 | #sign-up:hover, #logout:hover {
69 | transition: .5s;
70 | color: #000;
71 | background: #FFAA01;
72 | padding: 15px;
73 | border-radius: 35px;
74 | }
75 |
76 | #sign-up.active {
77 | color: #000;
78 | background: #FFAA01;
79 | }
80 |
81 | #running-simulator {
82 | margin-bottom: 3%;
83 | font-size: 16px;
84 | font-family: 'Josefin Sans', sans-serif;
85 | color: #fff;
86 | }
87 |
88 | .search-form {
89 | display: flex;
90 | flex-wrap: nowrap;
91 | flex-direction: column;
92 | }
93 | .search-list {
94 | position: absolute;
95 | background-color:#101010;
96 | width: 250px;
97 | margin: 0px;
98 | padding: 0px;
99 | border-radius: 5px;
100 | display: flex;
101 | flex-direction: column;
102 | }
103 |
104 | .hidden {
105 | display: none;
106 | }
107 |
108 | .search-result {
109 | height: 40px;
110 | padding: 0px;
111 | border-top: 1px white solid;
112 | display: flex;
113 | align-items: center;
114 | justify-content: flex-start;
115 | color: white;
116 | }
117 |
118 | .search-result > p {
119 | font-size: 10pt;
120 | }
121 |
122 | img.icon {
123 | height: 20px;
124 | width: 20px;
125 | margin: 0px 5px;
126 | }
127 |
128 | .search-input {
129 | width: 226px;
130 | margin: 5px 0px;
131 | }
132 |
133 | .search-input:focus ~ .search-results {
134 | display: block;
135 | }
136 |
137 | .visually-hidden {
138 | clip: rect(0 0 0 0);
139 | clip-path: inset(50%);
140 | height: 1px;
141 | overflow: hidden;
142 | position: absolute;
143 | white-space: nowrap;
144 | width: 1px;
145 | }
146 |
147 | .message {
148 | padding-top: 4%;
149 | text-align: center;
150 | font-family: 'Roboto', sans-serif;
151 | color: rgba(255, 255, 255, .7);
152 | font-size: 1.25em;
153 | cursor: default;
154 | }
155 |
156 |
157 |
158 | @media screen and (max-width: 870px) {
159 | #sign-up, #logout {
160 | padding: 10px;
161 | }
162 |
163 | #sign-up:hover, #logout:hover {
164 | padding: 10px;
165 | }
166 |
167 | /* .spacer{
168 | height: 35px;
169 | } */
170 | }
171 |
172 | /* Global Styles */
173 | #root {
174 | background: #000;
175 | }
176 |
177 | .content {
178 | background: #000;
179 | }
180 |
181 | h1, h2, h3, h4, h5, h6 {
182 | font-family: 'Josefin Sans', sans-serif;
183 | }
184 |
185 | p {
186 | font-family: 'Roboto', sans-serif;
187 | font-size: 18px;
188 | }
189 |
190 | button {
191 | padding: 15px;
192 | border: 0px;
193 | border-radius: 35px;
194 | background: #097A00;
195 | font-family: 'Josefin Sans', sans-serif;
196 | color: #fff;
197 | font-size: 18px;
198 | }
199 |
200 | button:hover {
201 | transition: .5s;
202 | color: #000;
203 | background: #FFAA01;
204 | border-radius: 35px;
205 | }
206 |
207 | /* Mobile Styles */
208 | #mobile {
209 | color: #fff;
210 | }
211 |
212 | /* Footer */
213 | footer .footer-top {
214 | display: flex;
215 | flex-direction: column wrap;
216 | justify-content: space-between;
217 | align-items: top;
218 | padding: 10px 4%;
219 | border-top: 2px solid #FFAA01;
220 | color: #fff;
221 | background: #000;
222 | }
223 |
224 | footer .footer-left {
225 | width: 25%;
226 | padding: 10px;
227 | margin: 0 auto;
228 | }
229 |
230 | footer .footer-middle {
231 | width: 50%;
232 | padding: 10px;
233 | margin: 0 auto;
234 | }
235 |
236 | footer .footer-right {
237 | width: 25%;
238 | padding: 10px;
239 | margin: 0 auto;
240 | }
241 |
242 | footer .bottom-bar {
243 | padding: 10px 4%;
244 | color: #fff;
245 | background: #000;
246 | }
247 |
248 | footer .bottom-bar hr {
249 | border: 1px solid #555;
250 | }
251 |
252 | footer h6 {
253 | margin: 8px 0;
254 | font-size: 16px;
255 | }
256 |
257 | footer p {
258 | font-size: 16px;
259 | line-height: 24px;
260 | }
261 |
262 | footer a {
263 | color: #fff;
264 | }
265 |
266 | footer a:hover {
267 | transition: .5s;
268 | color: #FFAA01;
269 | }
270 |
271 | @media screen and (max-width: 870px) {
272 |
273 | footer .footer-top {
274 | max-width: 100%;
275 | display: block;
276 | padding: 10px;
277 | }
278 |
279 | footer .footer-left {
280 | width: 95%;
281 | }
282 |
283 | footer .footer-middle {
284 | width: 95%;
285 | }
286 |
287 | footer .footer-right {
288 | width: 95%;
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/app/seeds/teams.py:
--------------------------------------------------------------------------------
1 | from app.models import db, Team
2 |
3 |
4 | def seed_team():
5 |
6 | team1 = Team(
7 | name='Boston Celtics',
8 | short_name='BOS',
9 | conference='Atlantic',
10 | shares='10000',
11 | price='50.00'
12 | )
13 | team2 = Team(
14 | name='Brooklyn Nets',
15 | short_name='BKN',
16 | conference='Atlantic',
17 | shares='10000',
18 | price='50.00'
19 | )
20 | team3 = Team(
21 | name='New York Knicks',
22 | short_name='NYK',
23 | conference='Atlantic',
24 | shares='10000',
25 | price='50.00'
26 | )
27 | team4 = Team(
28 | name='Philadelphia 76ers',
29 | short_name='PHI',
30 | conference='Atlantic',
31 | shares='10000',
32 | price='50.00'
33 | )
34 | team5 = Team(
35 | name='Toronto Raptors',
36 | short_name='TOR',
37 | conference='Atlantic',
38 | shares='10000',
39 | price='50.00'
40 | )
41 |
42 | team6 = Team(
43 | name='Chicago Bulls',
44 | short_name='CHI',
45 | conference='Central',
46 | shares='10000',
47 | price='50.00'
48 | )
49 | team7 = Team(
50 | name='Cleveland Cavaliers',
51 | short_name='CLE',
52 | conference='Central',
53 | shares='10000',
54 | price='50.00'
55 | )
56 | team8 = Team(
57 | name='Detroit Pistons',
58 | short_name='DET',
59 | conference='Central',
60 | shares='10000',
61 | price='50.00'
62 | )
63 | team9 = Team(
64 | name='Indiana Pacers',
65 | short_name='IND',
66 | conference='Central',
67 | shares='10000',
68 | price='50.00'
69 | )
70 | team10 = Team(
71 | name='Milwaukee Bucks',
72 | short_name='MIL',
73 | conference='Central',
74 | shares='10000',
75 | price='50.00'
76 | )
77 |
78 | team11 = Team(
79 | name='Atlanta Hawks',
80 | short_name='ATL',
81 | conference='Southeast',
82 | shares='10000',
83 | price='50.00'
84 | )
85 | team12 = Team(
86 | name='Charlotte Hornets',
87 | short_name='CHA',
88 | conference='Southeast',
89 | shares='10000',
90 | price='50.00'
91 | )
92 | team13 = Team(
93 | name='Miami Heat',
94 | short_name='MIA',
95 | conference='Southeast',
96 | shares='10000',
97 | price='50.00'
98 | )
99 | team14 = Team(
100 | name='Orlando Magic',
101 | short_name='ORL',
102 | conference='Southeast',
103 | shares='10000',
104 | price='50.00'
105 | )
106 | team15 = Team(
107 | name='Washington Wizards',
108 | short_name='WAS',
109 | conference='Southeast',
110 | shares='10000',
111 | price='50.00'
112 | )
113 |
114 | team16 = Team(
115 | name='Denver Nuggets',
116 | short_name='DEN',
117 | conference='Northwest',
118 | shares='10000',
119 | price='50.00'
120 | )
121 | team17 = Team(
122 | name='Minnesota Timberwolves',
123 | short_name='MIN',
124 | conference='Northwest',
125 | shares='10000',
126 | price='50.00'
127 | )
128 | team18 = Team(
129 | name='Oklahoma City Thunder',
130 | short_name='OKC',
131 | conference='Northwest',
132 | shares='10000',
133 | price='50.00'
134 | )
135 | team19 = Team(
136 | name='Portland Trail Blazers',
137 | short_name='POR',
138 | conference='Northwest',
139 | shares='10000',
140 | price='50.00'
141 | )
142 | team20 = Team(
143 | name='Utah Jazz',
144 | short_name='UTH',
145 | conference='Northwest',
146 | shares='10000',
147 | price='50.00'
148 | )
149 |
150 | team21 = Team(
151 | name='Golden State Warriors',
152 | short_name='GSW',
153 | conference='Pacific',
154 | shares='10000',
155 | price='50.00'
156 | )
157 | team22 = Team(
158 | name='LA Clippers',
159 | short_name='LAC',
160 | conference='Pacific',
161 | shares='10000',
162 | price='50.00'
163 | )
164 | team23 = Team(
165 | name='Los Angeles Lakers',
166 | short_name='LAL',
167 | conference='Pacific',
168 | shares='10000',
169 | price='50.00'
170 | )
171 | team24 = Team(
172 | name='Phoenix Suns',
173 | short_name='PHO',
174 | conference='Pacific',
175 | shares='10000',
176 | price='50.00'
177 | )
178 | team25 = Team(
179 | name='Sacramento Kings',
180 | short_name='SAC',
181 | conference='Pacific',
182 | shares='10000',
183 | price='50.00'
184 | )
185 |
186 | team26 = Team(
187 | name='Dallas Mavericks',
188 | short_name='DAL',
189 | conference='Southwest',
190 | shares='10000',
191 | price='50.00'
192 | )
193 | team27 = Team(
194 | name='Houston Rockets',
195 | short_name='HOU',
196 | conference='Southwest',
197 | shares='10000',
198 | price='50.00'
199 | )
200 | team28 = Team(
201 | name='Memphis Grizzlies',
202 | short_name='MEM',
203 | conference='Southwest',
204 | shares='10000',
205 | price='50.00'
206 | )
207 | team29 = Team(
208 | name='New Orleans Pelicans',
209 | short_name='NOH',
210 | conference='Southwest',
211 | shares='10000',
212 | price='50.00'
213 | )
214 | team30 = Team(
215 | name='San Antonio Spurs',
216 | short_name='SAN',
217 | conference='Southwest',
218 | shares='10000',
219 | price='50.00'
220 | )
221 |
222 | db.session.add(team1)
223 | db.session.add(team2)
224 | db.session.add(team3)
225 | db.session.add(team4)
226 | db.session.add(team5)
227 | db.session.add(team6)
228 | db.session.add(team7)
229 | db.session.add(team8)
230 | db.session.add(team9)
231 | db.session.add(team10)
232 | db.session.add(team11)
233 | db.session.add(team12)
234 | db.session.add(team13)
235 | db.session.add(team14)
236 | db.session.add(team15)
237 | db.session.add(team16)
238 | db.session.add(team17)
239 | db.session.add(team18)
240 | db.session.add(team19)
241 | db.session.add(team20)
242 | db.session.add(team21)
243 | db.session.add(team22)
244 | db.session.add(team23)
245 | db.session.add(team24)
246 | db.session.add(team25)
247 | db.session.add(team26)
248 | db.session.add(team27)
249 | db.session.add(team28)
250 | db.session.add(team29)
251 | db.session.add(team30)
252 | db.session.commit()
253 |
254 |
255 | def undo_team(
256 |
257 | ):
258 | db.session.execute('TRUNCATE Teams RESTART IDENTITY CASCADE;')
259 | db.session.commit()
260 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Robinhoop
2 |
3 |
4 | Access the app [here](https://robinhoop.herokuapp.com/).
5 |
6 |
7 | Robinhoop is a simulated clone of the Robinhood Stock Trading appfocused on Basketball. With Robinhoop, users can invest in their favorite basketball teams (or the ones that are actually winning games :smirk:). Robinhoop users can buy, sell, and keep tabs on NBA basketball teams to see which ones they want to buy or sell their stock in.
8 |
9 | Robinhoop is a simulated basketball stock market, not an actual stock market. No real currency is won, lost, or exchanged by using this app. It's mostly just for fun.
10 |
11 |
12 | # Technologies Used
13 |
14 |
15 | * Python 3.9.4
16 | * Javascript ES6
17 | * React.js
18 | * Flask
19 | * react-chartjs
20 | * PostgreSQL
21 | * Heroku
22 |
23 |
24 | # Documentation
25 |
26 |
27 | [Github Repository Wiki](https://github.com/NCHitsman/Python-Group-Project-Team-7/wiki)
28 |
29 |
30 | # Features
31 |
32 |
33 | * Sign up a new account
34 | * Log in and log out
35 | * Edit account login information
36 | * Search for stocks by name, abbreviation, or conference name
37 | * View stock prices and events on a portfolio
38 | * View a graphic representation of a stock's details
39 | * View a listing of all available teams and their related information
40 | * Keep tabs on teams using a watchlist
41 |
42 |
43 | # Screenshots
44 |
45 |
46 | A user who is not logged in sees a page summarizing the app and what it is used for.
47 |
48 |
49 |
50 | If a user does not have an existing account, they may create one.
51 |
52 |
53 |
54 | A user may log in to their account to access features of the app.
55 |
56 |
57 |
58 | Account information can be changed as needed by the user logged in.
59 |
60 |
61 |
62 | The user is able to search for teams they are looking for using relevant queries.
63 |
64 |
65 |
66 | On the portfolio page, the user can view different stock prices and the events that are affecting those prices.
67 |
68 |
69 |
70 | Each team has its own details page, where there is a graphic representation of the price trend. This is also where a user can buy and sell shares.
71 |
72 |
73 |
74 | The buy page displays the teams and more details about them.
75 |
76 |
77 |
78 | A user can keep tabs on teams and stocks they are interested in using a watchlist.
79 |
80 |
81 |
82 | # Technical Details
83 | Robinhoop uses a Python based Flask backend with a Javascript based React frontend. The API routes handle interaction with the PostgreSQL database.
84 |
85 |
86 | The account related routes are protected with user authentication, requiring a user to be logged in to manipulate information related to that account.
87 |
88 |
89 | The navigation bar contains a search bar, where a user can query for certain teams based on the team name, abbreviation, or conference name. The results are shown only when the search bar is active, so other elements on the page are not visually blocked.
90 |
91 |
92 | ```
93 | ...
94 |
95 | const filterTeams = (teams, query) => {
96 | if (!query) {
97 | return [];
98 | }
99 |
100 | return teams.filter((team) => {
101 | const teamName = team.name.toLowerCase();
102 | const teamAbbr = team.short_name.toLowerCase();
103 | const teamConf = team.conference.toLowerCase();
104 | return teamName.includes(query.toLowerCase()) || teamAbbr.includes(query.toLowerCase()) || teamConf.includes(query.toLowerCase());
105 | })
106 | }
107 |
108 | ...
109 | ```
110 |
111 |
112 | Data fetched from the API routes are used to create teams, their related stocks, and events that manage changes in stock value. A visual representation of these changes is created using react-charjs. The line color depends on how the prices have changed from their previous value.
113 |
114 |
115 | ```
116 | const GraphCanvas = ({ history }) => {
117 |
118 | const ref = useRef()
119 |
120 | const down = (context, value) => context.p0.parsed.y > context.p1.parsed.y ? value : undefined;
121 |
122 | const data = {
123 | labels: [],
124 | datasets: [
125 | {
126 | label: 'Price',
127 | data: [],
128 | fill: false,
129 | backgroundColor: 'black',
130 | borderColor: 'green',
131 | segment: {
132 | borderColor: context => down(context, 'red'),
133 | }
134 | },
135 | ],
136 | };
137 |
138 | let lastHistory;
139 | for (let i = history?.length - 1; i >=0 ; i--) {
140 | if (history[i].price !== lastHistory) {
141 | data.labels.push(`${history[i].date.slice(8 , 11)} ${history[i].date.slice(5, 7)}`)
142 | data.datasets[0].data.push(history[i].price)
143 | lastHistory = history[i].price
144 | }
145 | }
146 |
147 | ...
148 | ```
149 |
150 |
151 | The simulation can be started and stopped with a button in the navigation bar, where events and changes in stock value happen in real-time. A loop is used to randomly generate the events and respective scores for the participating teams, and the scores are used in an algorithm to affect stock values.
152 |
153 |
154 | ```
155 | ...
156 |
157 | let home = teamIds.splice(Math.floor(Math.random() * teamIds.length), 1)
158 |
159 | let away = teamIds.splice(Math.floor(Math.random() * teamIds.length), 1)
160 |
161 | home.push(Math.round(Math.random() * (125 - 75) + 75))
162 | away.push(Math.round(Math.random() * (125 - 75) + 75))
163 | let diff;
164 |
165 | if (home[1] > away[1]) {
166 | await newArticle(home[0], away[0], home[1], away[1])
167 | diff = home[1] - away[1]
168 | home.push(true)
169 | } else {
170 | await newArticle(away[0], home[0], away[1], home[1])
171 | diff = away[1] - home[1]
172 | home.push(true)
173 | }
174 |
175 | await makeStockHistory(home[1])
176 | await makeStockHistory(away[1])
177 |
178 | await updateStock(home[1], diff, home[2])
179 | await updateStock(away[1], diff, away[2])
180 |
181 | ...
182 | ```
183 |
184 |
185 | The `` page shows all the available teams and their relevant information. There is also a link to view each team in detail, where the user can buy and sell stocks for that team. Due to potential copyright issues, a placeholder image is used in place of the team logo.
186 |
187 |
188 | On a team's detail page, a user can select the number of stocks they would like to buy or sell. The total price is reflected by the number of shares and the share value. Again, due to potential copyright issues, a placeholder image is used in place of the team logo.
189 |
190 |
191 | A user can also add or remove teams on their watchlist, where they can view only the teams they are interested in.
192 |
193 |
194 | # Todo:
195 |
196 |
197 | * Buy and sell stocks
198 | * Add stocks on a watchlist
199 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "a2ab2b71d5ba8d95df02368eab9e979d83e5e79ba0e6d9e41b98c589811ba5d8"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.9"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "alembic": {
20 | "hashes": [
21 | "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c",
22 | "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245"
23 | ],
24 | "index": "pypi",
25 | "version": "==1.4.3"
26 | },
27 | "click": {
28 | "hashes": [
29 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
30 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
31 | ],
32 | "index": "pypi",
33 | "version": "==7.1.2"
34 | },
35 | "dnspython": {
36 | "hashes": [
37 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216",
38 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"
39 | ],
40 | "markers": "python_version >= '3.6'",
41 | "version": "==2.1.0"
42 | },
43 | "email-validator": {
44 | "hashes": [
45 | "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b",
46 | "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"
47 | ],
48 | "index": "pypi",
49 | "version": "==1.1.3"
50 | },
51 | "flask": {
52 | "hashes": [
53 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
54 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
55 | ],
56 | "index": "pypi",
57 | "version": "==1.1.2"
58 | },
59 | "flask-cors": {
60 | "hashes": [
61 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
62 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
63 | ],
64 | "index": "pypi",
65 | "version": "==3.0.8"
66 | },
67 | "flask-jwt-extended": {
68 | "hashes": [
69 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd"
70 | ],
71 | "index": "pypi",
72 | "version": "==3.24.1"
73 | },
74 | "flask-login": {
75 | "hashes": [
76 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
77 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
78 | ],
79 | "index": "pypi",
80 | "version": "==0.5.0"
81 | },
82 | "flask-migrate": {
83 | "hashes": [
84 | "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732",
85 | "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee"
86 | ],
87 | "index": "pypi",
88 | "version": "==2.5.3"
89 | },
90 | "flask-sqlalchemy": {
91 | "hashes": [
92 | "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
93 | "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
94 | ],
95 | "index": "pypi",
96 | "version": "==2.4.4"
97 | },
98 | "flask-wtf": {
99 | "hashes": [
100 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
101 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
102 | ],
103 | "index": "pypi",
104 | "version": "==0.14.3"
105 | },
106 | "gunicorn": {
107 | "hashes": [
108 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
109 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
110 | ],
111 | "index": "pypi",
112 | "version": "==20.0.4"
113 | },
114 | "idna": {
115 | "hashes": [
116 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
117 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
118 | ],
119 | "markers": "python_version >= '3.5'",
120 | "version": "==3.2"
121 | },
122 | "itsdangerous": {
123 | "hashes": [
124 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
125 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
126 | ],
127 | "index": "pypi",
128 | "version": "==1.1.0"
129 | },
130 | "jinja2": {
131 | "hashes": [
132 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
133 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
134 | ],
135 | "index": "pypi",
136 | "version": "==2.11.2"
137 | },
138 | "mako": {
139 | "hashes": [
140 | "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27",
141 | "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"
142 | ],
143 | "index": "pypi",
144 | "version": "==1.1.3"
145 | },
146 | "markupsafe": {
147 | "hashes": [
148 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
149 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
150 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
151 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
152 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
153 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
154 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
155 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
156 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
157 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
158 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
159 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
160 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
161 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
162 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
163 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
164 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
165 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
166 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
167 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
168 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
169 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
170 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
171 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
172 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
173 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
174 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
175 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
176 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
177 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
178 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
179 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
180 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
181 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
182 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
183 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
184 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
185 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
186 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
187 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
188 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
189 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
190 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
191 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
192 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
193 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
194 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
195 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
196 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
197 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
198 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
199 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
200 | ],
201 | "index": "pypi",
202 | "version": "==1.1.1"
203 | },
204 | "pyjwt": {
205 | "hashes": [
206 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
207 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
208 | ],
209 | "index": "pypi",
210 | "version": "==1.7.1"
211 | },
212 | "python-dateutil": {
213 | "hashes": [
214 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
215 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
216 | ],
217 | "index": "pypi",
218 | "version": "==2.8.1"
219 | },
220 | "python-dotenv": {
221 | "hashes": [
222 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
223 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
224 | ],
225 | "index": "pypi",
226 | "version": "==0.14.0"
227 | },
228 | "python-editor": {
229 | "hashes": [
230 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
231 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
232 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8",
233 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77",
234 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"
235 | ],
236 | "index": "pypi",
237 | "version": "==1.0.4"
238 | },
239 | "six": {
240 | "hashes": [
241 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
242 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
243 | ],
244 | "index": "pypi",
245 | "version": "==1.15.0"
246 | },
247 | "sqlalchemy": {
248 | "hashes": [
249 | "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb",
250 | "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804",
251 | "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6",
252 | "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0",
253 | "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe",
254 | "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de",
255 | "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36",
256 | "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e",
257 | "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66",
258 | "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6",
259 | "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc",
260 | "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d",
261 | "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce",
262 | "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea",
263 | "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f",
264 | "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365",
265 | "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea",
266 | "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23",
267 | "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338",
268 | "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1",
269 | "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b",
270 | "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e",
271 | "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba",
272 | "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02",
273 | "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12",
274 | "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86",
275 | "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d",
276 | "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7",
277 | "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0",
278 | "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac",
279 | "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc",
280 | "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37"
281 | ],
282 | "index": "pypi",
283 | "version": "==1.3.19"
284 | },
285 | "werkzeug": {
286 | "hashes": [
287 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
288 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
289 | ],
290 | "index": "pypi",
291 | "version": "==1.0.1"
292 | },
293 | "wtforms": {
294 | "hashes": [
295 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c",
296 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
297 | ],
298 | "index": "pypi",
299 | "version": "==2.3.3"
300 | }
301 | },
302 | "develop": {
303 | "astroid": {
304 | "hashes": [
305 | "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
306 | "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
307 | ],
308 | "markers": "python_version ~= '3.6'",
309 | "version": "==2.5.6"
310 | },
311 | "autopep8": {
312 | "hashes": [
313 | "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0",
314 | "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"
315 | ],
316 | "index": "pypi",
317 | "version": "==1.5.7"
318 | },
319 | "isort": {
320 | "hashes": [
321 | "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
322 | "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
323 | ],
324 | "markers": "python_full_version >= '3.6.1' and python_version < '4.0'",
325 | "version": "==5.9.1"
326 | },
327 | "lazy-object-proxy": {
328 | "hashes": [
329 | "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653",
330 | "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61",
331 | "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2",
332 | "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837",
333 | "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3",
334 | "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43",
335 | "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726",
336 | "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3",
337 | "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587",
338 | "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8",
339 | "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a",
340 | "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd",
341 | "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f",
342 | "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad",
343 | "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4",
344 | "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b",
345 | "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf",
346 | "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981",
347 | "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741",
348 | "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e",
349 | "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
350 | "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
351 | ],
352 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
353 | "version": "==1.6.0"
354 | },
355 | "mccabe": {
356 | "hashes": [
357 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
358 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
359 | ],
360 | "version": "==0.6.1"
361 | },
362 | "psycopg2-binary": {
363 | "hashes": [
364 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
365 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
366 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
367 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
368 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
369 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
370 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
371 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
372 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
373 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
374 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
375 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
376 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
377 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
378 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
379 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
380 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
381 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
382 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
383 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
384 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
385 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
386 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
387 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
388 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
389 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
390 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
391 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
392 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
393 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
394 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
395 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
396 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
397 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
398 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
399 | ],
400 | "index": "pypi",
401 | "version": "==2.8.6"
402 | },
403 | "pycodestyle": {
404 | "hashes": [
405 | "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
406 | "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
407 | ],
408 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
409 | "version": "==2.7.0"
410 | },
411 | "pylint": {
412 | "hashes": [
413 | "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8",
414 | "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"
415 | ],
416 | "index": "pypi",
417 | "version": "==2.8.3"
418 | },
419 | "toml": {
420 | "hashes": [
421 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
422 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
423 | ],
424 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
425 | "version": "==0.10.2"
426 | },
427 | "wrapt": {
428 | "hashes": [
429 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
430 | ],
431 | "version": "==1.12.1"
432 | }
433 | }
434 | }
435 |
--------------------------------------------------------------------------------