├── .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 ; 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 | 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 |
      {userComponents}
    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 | 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 | 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 | 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 | 32 | {watchlist && 33 | } 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 | Robinhoop Investing Logo 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 | 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 | 29 |
    30 | 31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 | graph 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 | 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 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 | {errors.map((error) => ( 51 |
    {error}
    52 | ))} 53 |
    54 |
    55 |

    Sign in

    56 |

    Don't have a Robinhoop account? Join Now

    57 |
    58 |
    59 | 60 | 67 |
    68 |
    69 | 70 | 77 |
    78 | 79 |
    80 |
    81 | 82 |
    83 |
    84 |
    85 |
    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 |
    21 |

    Team

    22 |
    23 |
    24 |

    25 |
    26 |
    27 |

    28 |
    29 |
    30 |

    Conference

    31 |
    32 |
    33 |

    Shares

    34 |
    35 |
    36 |

    Share Value

    37 |
    38 |
    39 |

    Page

    40 |
    41 |
    42 | {Object.values(teams)?.map((team) => { 43 | return ( 44 |
    45 |
    46 | 47 |
    48 |
    49 |

    {team.name}

    50 |
    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 |
    48 | {/*
    */} 49 | {/* */} 52 | setSearchQuery(e.target.value)} 55 | type="text" 56 | id="header-search" 57 | placeholder="Search teams" 58 | name="s" 59 | className="search-input" 60 | onClick={() => setHidden(false)} 61 | /> 62 | {/* */} 63 |
    64 |
      65 | {filteredTeams.map((team) => ( 66 |
    • { 68 | await history.push(`/stock/${team.id}`) 69 | setHidden(true) 70 | setSearchQuery('') 71 | }} 72 | key={team.id} className="search-result" id={team.id}> 73 | 74 |

      {team.name} ({team.short_name})

      75 |
    • 76 | ))} 77 |
    78 |
    79 | {/*
    */} 80 |
    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 |