├── dev-requirements.txt ├── .flaskenv ├── migrations ├── README ├── .DS_Store ├── script.py.mako ├── versions │ ├── 20210221_123637_updated_spots_table.py │ ├── 20210225_163546_.py │ ├── 20210221_133750_.py │ ├── 20201120_150602_create_users_table.py │ ├── 20210221_124947_create_user_book_spot_table.py │ └── 20210220_134806_create_spots_table.py ├── alembic.ini └── env.py ├── react-app ├── src │ ├── components │ │ ├── SpotComponent │ │ │ ├── SpotNewest │ │ │ │ └── index.js │ │ │ ├── SpotViewMini │ │ │ │ ├── SpotViewMini.css │ │ │ │ └── index.js │ │ │ ├── SpotViewLarge │ │ │ │ ├── SpotViewLarge.css │ │ │ │ └── index.js │ │ │ ├── SpotCreate │ │ │ │ ├── SpotCreate.css │ │ │ │ └── index.js │ │ │ ├── SpotEditComponent │ │ │ │ └── index.js │ │ │ └── SpotPage │ │ │ │ ├── SpotPage.css │ │ │ │ └── index.js │ │ ├── GoogleMapsComponent │ │ │ ├── marker_logo.png │ │ │ ├── GoogleMap.css │ │ │ ├── index.js │ │ │ └── mapStyles.js │ │ ├── FooterComponent │ │ │ ├── FooterComponent.css │ │ │ └── index.js │ │ ├── auth │ │ │ ├── LogoutButton.js │ │ │ ├── ProtectedRoute.js │ │ │ ├── LoginForm.js │ │ │ └── SignUpForm.js │ │ ├── User.js │ │ ├── HomePageComponent │ │ │ ├── newUser.js │ │ │ ├── home-page.css │ │ │ └── index.js │ │ ├── SearchComponent │ │ │ ├── SearchPage │ │ │ │ ├── search-page.css │ │ │ │ ├── searchmap.js │ │ │ │ └── index.js │ │ │ └── SearchBar │ │ │ │ ├── search-bar.css │ │ │ │ └── index.js │ │ ├── FundingComponent │ │ │ └── index.js │ │ ├── BookingPageComponent │ │ │ └── index.js │ │ ├── UsersList.js │ │ └── NavBar.js │ ├── index.js │ ├── aws │ │ └── s3.js │ ├── index.css │ ├── store │ │ ├── index.js │ │ ├── booking.js │ │ ├── funding.js │ │ └── spot.js │ ├── services │ │ └── auth.js │ └── App.js ├── .env.example ├── public │ ├── favicon.ico │ ├── linkedIn.png │ └── index.html ├── .gitignore ├── README.md └── package.json ├── .DS_Store ├── app ├── .DS_Store ├── models │ ├── db.py │ ├── __init__.py │ ├── funding.py │ ├── user_book_spot.py │ ├── user.py │ └── spot.py ├── forms │ ├── __init__.py │ ├── booking_form.py │ ├── funding_form.py │ ├── signup_form.py │ ├── login_form.py │ └── spot_form.py ├── config.py ├── api │ ├── user_routes.py │ ├── funding_routes.py │ ├── booking_routes.py │ ├── spot_routes.py │ └── auth_routes.py ├── seeds │ ├── fundings.py │ ├── user_book_spots.py │ ├── __init__.py │ ├── users.py │ └── spots.py └── __init__.py ├── .envexample ├── .vscode └── settings.json ├── Dockerfile ├── Pipfile ├── requirements.txt ├── .gitignore ├── README.md └── Pipfile.lock /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.6 2 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotNewest/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-app/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:5000 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintinHull/CareBnB/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintinHull/CareBnB/HEAD/app/.DS_Store -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /.envexample: -------------------------------------------------------------------------------- 1 | FLASK_ENV = 2 | SECRET_KEY= 3 | DATABASE_URL= 4 | ACCESS_KEY_ID= 5 | SECRET_ACCESS_KEY= -------------------------------------------------------------------------------- /migrations/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintinHull/CareBnB/HEAD/migrations/.DS_Store -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotViewMini/SpotViewMini.css: -------------------------------------------------------------------------------- 1 | .spotcard { 2 | border: none; 3 | } -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintinHull/CareBnB/HEAD/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/linkedIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintinHull/CareBnB/HEAD/react-app/public/linkedIn.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pycodestyleEnabled": false, 3 | "python.linting.pylintEnabled": true, 4 | "python.linting.enabled": true 5 | } -------------------------------------------------------------------------------- /react-app/src/components/GoogleMapsComponent/marker_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuintinHull/CareBnB/HEAD/react-app/src/components/GoogleMapsComponent/marker_logo.png -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .spot import Spot 4 | from .user_book_spot import User_Book_Spot 5 | from .funding import Funding 6 | -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm 3 | from .spot_form import SpotForm 4 | from .funding_form import FundingForm 5 | from .booking_form import BookingForm -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class Config: 4 | SECRET_KEY=os.environ.get('SECRET_KEY') 5 | SQLALCHEMY_TRACK_MODIFICATIONS=False 6 | SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URL') 7 | SQLALCHEMY_ECHO=True -------------------------------------------------------------------------------- /react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /app/forms/booking_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField 3 | from wtforms.validators import DataRequired 4 | from app.models import User_Book_Spot 5 | 6 | 7 | class BookingForm(FlaskForm): 8 | group_size = IntegerField("group_size", validators=[DataRequired()]) 9 | -------------------------------------------------------------------------------- /app/forms/funding_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField 3 | from wtforms.validators import DataRequired 4 | from app.models import Funding 5 | 6 | 7 | class FundingForm(FlaskForm): 8 | sponsorship_cost = IntegerField("sponsorship_cost", validators=[DataRequired()]) 9 | -------------------------------------------------------------------------------- /react-app/src/components/GoogleMapsComponent/GoogleMap.css: -------------------------------------------------------------------------------- 1 | .spot-view-map .spotcard { 2 | background-color: #DAE2DF; 3 | padding: 20px; 4 | margin: 10px; 5 | min-width: 250px; 6 | max-width: 250px; 7 | max-height: 350x; 8 | box-shadow: 0 1px 5px #A2A7A5; 9 | transition: all .2s ease-in-out; 10 | } 11 | 12 | .spot-view-map p.card-text { 13 | display: none; 14 | } 15 | -------------------------------------------------------------------------------- /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/aws/s3.js: -------------------------------------------------------------------------------- 1 | import S3 from 'react-aws-s3'; 2 | import { ACCESS_KEY_ID, SECRET_ACCESS_KEY } from './keys'; 3 | require('dotenv').config() 4 | 5 | const config = { 6 | bucketName: 'carebnb', 7 | dirName: 'spot_pictures', 8 | region: 'us-east-2', 9 | accessKeyId: ACCESS_KEY_ID, 10 | secretAccessKey: SECRET_ACCESS_KEY 11 | } 12 | 13 | const UploadPictureS3Client = new S3(config); 14 | 15 | export default UploadPictureS3Client; -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotViewLarge/SpotViewLarge.css: -------------------------------------------------------------------------------- 1 | .spot-view-large-body{ 2 | overflow: hidden; 3 | position: relative; 4 | background-color: white; 5 | padding: 0; 6 | border-color: coral; 7 | } 8 | 9 | .spot-view-large-image{ 10 | width: 100%; 11 | height: auto; 12 | 13 | 14 | overflow: hidden; 15 | } 16 | 17 | .spot-view-large-text{ 18 | position: absolute; 19 | top: 8px; 20 | left: 16px; 21 | } -------------------------------------------------------------------------------- /app/api/user_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask_login import login_required 3 | from app.models import User 4 | 5 | user_routes = Blueprint('users', __name__) 6 | 7 | 8 | @user_routes.route('/') 9 | @login_required 10 | def users(): 11 | users = User.query.all() 12 | return {"users": [user.to_dict() for user in users]} 13 | 14 | 15 | @user_routes.route('/') 16 | @login_required 17 | def user(id): 18 | user = User.query.get(id) 19 | return user.to_dict() 20 | -------------------------------------------------------------------------------- /react-app/src/components/FooterComponent/FooterComponent.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | height: 70px; 3 | width: 100%; 4 | background-color: rgb(226, 218, 219); 5 | display: flex; 6 | justify-content: space-around; 7 | } 8 | 9 | .image_container { 10 | margin:auto; 11 | } 12 | 13 | .pic { 14 | /* width: 35px; */ 15 | height: 60px; 16 | border-radius: 5px; 17 | margin-right: 5px; 18 | } 19 | 20 | .the_developers { 21 | margin:auto; 22 | font-family: 'Martel Sans'; 23 | font-weight: 900; 24 | font-size: 20px; 25 | } -------------------------------------------------------------------------------- /react-app/src/components/auth/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "react-bootstrap" 3 | import { useHistory } from "react-router"; 4 | import { logout } from "../../services/auth"; 5 | 6 | const LogoutButton = ({ setAuthenticated }) => { 7 | const history = useHistory(); 8 | 9 | const onLogout = async (e) => { 10 | history.push('/'); 11 | await logout(); 12 | setAuthenticated(false); 13 | }; 14 | 15 | return ; 16 | }; 17 | 18 | export default LogoutButton; 19 | -------------------------------------------------------------------------------- /react-app/src/components/auth/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, useHistory } from 'react-router-dom'; 3 | 4 | const ProtectedRoute = props => { 5 | const history = useHistory(); 6 | const unprotected = (e) => { 7 | history.push('/') 8 | props.setShowSignUp(true) 9 | }; 10 | 11 | return ( 12 | 13 | {props.authenticated && props.children} 14 | {!props.authenticated && unprotected()} 15 | 16 | ) 17 | }; 18 | 19 | 20 | export default ProtectedRoute; 21 | -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | /* TODO Add site wide styles */ 2 | html { 3 | min-width: 1000px; 4 | } 5 | 6 | .btn:hover { 7 | background-color: #6D696A; 8 | } 9 | 10 | .logo { 11 | width: 100px; 12 | height: auto; 13 | 14 | } 15 | 16 | h1, h2 { 17 | font-family: 'Josefin Sans'; 18 | } 19 | 20 | h3, h4, h5 { 21 | font-family: 'Marcellus'; 22 | } 23 | 24 | a, p, button, .btn, .card-title, label { 25 | font-family: 'Martel Sans'; 26 | } 27 | 28 | 29 | .dropdown-menu.show { 30 | position: absolute; 31 | width: 350px; 32 | left: -285px; 33 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotViewLarge/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import './SpotViewLarge.css' 3 | 4 | const SpotViewLarge = ({ spot }) => { 5 | return ( 6 |
7 | 8 | 9 |

{spot.description}

10 |

{spot.title}

11 |

{spot.city}, {spot.state}

12 |

{spot.availability}

13 | Spot Image 14 | 15 |
16 | ) 17 | } 18 | 19 | export default SpotViewLarge -------------------------------------------------------------------------------- /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/seeds/fundings.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Funding 2 | 3 | 4 | def seed_fundings(): 5 | funding_1 = Funding(user_id=1, spots_id=1, sponsorship_cost=10.0) 6 | 7 | funding_2 = Funding(user_id=1, spots_id=2, sponsorship_cost=15.0) 8 | 9 | funding_3 = Funding(user_id=1, spots_id=3, sponsorship_cost=5.0) 10 | 11 | funding_4 = Funding(user_id=1, spots_id=4, sponsorship_cost=20.0) 12 | 13 | db.session.add(funding_1) 14 | db.session.add(funding_2) 15 | db.session.add(funding_3) 16 | db.session.add(funding_4) 17 | db.session.commit() 18 | 19 | 20 | def undo_fundings(): 21 | Funding.query.delete() 22 | db.session.commit() 23 | -------------------------------------------------------------------------------- /app/models/funding.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class Funding(db.Model): 5 | __tablename__ = 'fundings' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) 9 | spots_id = db.Column(db.Integer, db.ForeignKey("spots.id"), nullable=False) 10 | sponsorship_cost = db.Column(db.Float) 11 | 12 | sponsor = db.relationship("User") 13 | spot = db.relationship("Spot") 14 | 15 | def to_dict(self): 16 | return { 17 | 'id': self.id, 18 | 'sponsor': self.sponsor.to_dict(), 19 | 'spot': self.spot.to_dict(), 20 | 'sponsorship_cost': self.sponsorship_cost 21 | } -------------------------------------------------------------------------------- /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://git.heroku.com/care-b-n-b.git 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/models/user_book_spot.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class User_Book_Spot(db.Model): 5 | __tablename__ = "user_book_spots" 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | spots_id = db.Column(db.Integer, db.ForeignKey("spots.id"), nullable=False) 9 | guest_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) 10 | group_size = db.Column(db.Integer, nullable=False) 11 | 12 | spot = db.relationship("Spot") 13 | guest = db.relationship("User") 14 | 15 | def to_dict(self): 16 | return { 17 | 'id': self.id, 18 | 'spot': self.spot.to_dict(), 19 | 'guest': self.guest.to_dict(), 20 | 'group_size': self.group_size 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 | first_name = StringField('first_name', validators=[DataRequired()]) 17 | last_name = StringField('last_name', validators=[DataRequired()]) 18 | email = StringField('email', validators=[DataRequired(), user_exists]) 19 | password = StringField('password', validators=[DataRequired()]) 20 | -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | CareBnB 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /migrations/versions/20210221_123637_updated_spots_table.py: -------------------------------------------------------------------------------- 1 | """updated spots table 2 | 3 | Revision ID: f0f68448b41c 4 | Revises: 2843224166c6 5 | Create Date: 2021-02-21 12:36:37.446435 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f0f68448b41c' 14 | down_revision = '2843224166c6' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('spots', sa.Column('zipcode', sa.Integer(), nullable=False)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('spots', 'zipcode') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /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.8" 38 | -------------------------------------------------------------------------------- /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; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 10 | click==7.1.2 11 | flask-cors==3.0.8 12 | flask-jwt-extended==3.24.1 13 | flask-migrate==2.5.3 14 | flask-login==0.5.0 15 | flask-sqlalchemy==2.4.4 16 | flask-wtf==0.14.3 17 | flask==1.1.2 18 | gunicorn==20.0.4 19 | itsdangerous==1.1.0 20 | jinja2==2.11.2 21 | mako==1.1.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 22 | markupsafe==1.1.1 23 | pyjwt==1.7.1 24 | python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 25 | python-dotenv==0.14.0 26 | python-editor==1.0.4 27 | six==1.15.0 28 | sqlalchemy==1.3.19 29 | werkzeug==1.0.1 30 | wtforms==2.3.3 31 | -------------------------------------------------------------------------------- /migrations/versions/20210225_163546_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: a92ee759c9c8 4 | Revises: 3430e93927f7 5 | Create Date: 2021-02-25 16:35:46.983791 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a92ee759c9c8' 14 | down_revision = '3430e93927f7' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('spots', sa.Column('latitude', sa.Float(), nullable=True)) 22 | op.add_column('spots', sa.Column('longitude', sa.Float(), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('spots', 'longitude') 29 | op.drop_column('spots', 'latitude') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /react-app/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import spotReducer from "./spot"; 4 | import bookingReducer from "./booking"; 5 | import fundingReducer from "./funding"; 6 | 7 | const rootReducer = combineReducers({ 8 | spots: spotReducer, 9 | bookings: bookingReducer, 10 | fundings: fundingReducer, 11 | }); 12 | 13 | let enhancer; 14 | 15 | if (process.env.NODE_ENV === "production") { 16 | enhancer = applyMiddleware(thunk); 17 | } else { 18 | const logger = require("redux-logger").default; 19 | const composeEnhancers = 20 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 21 | enhancer = composeEnhancers(applyMiddleware(thunk, logger)); 22 | } 23 | 24 | const configureStore = (preloadedState) => { 25 | return createStore(rootReducer, preloadedState, enhancer); 26 | }; 27 | 28 | export default configureStore; 29 | -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotCreate/SpotCreate.css: -------------------------------------------------------------------------------- 1 | .spot-create-body { 2 | margin-top: 15px; 3 | margin-bottom: 15px; 4 | } 5 | 6 | .spot-create-container { 7 | background-color: #dae2df; 8 | /* border: solid #e2dadb; */ 9 | border-radius: 5px; 10 | box-shadow: 1px 1px 1px rgb(172, 171, 171); 11 | } 12 | 13 | .spot-create-input { 14 | background-color: #ffffff; 15 | } 16 | 17 | .spot-create-header { 18 | display: flex; 19 | justify-content: center; 20 | margin-top: 15px; 21 | font-size: 35px; 22 | font-family: "Josefin Sans", sans-serif; 23 | } 24 | 25 | .spot-create-zipcode::-webkit-inner-spin-button, 26 | .spot-create-zipcode::-webkit-inner-spin-button { 27 | -webkit-appearance: none; 28 | margin: 0; 29 | } 30 | 31 | .spot-create-button { 32 | display: flex; 33 | justify-content: center; 34 | margin-bottom: 15px; 35 | } 36 | 37 | .spot-create-button button { 38 | width: 100px; 39 | background-color: #6d696a; 40 | color: #ffffff; 41 | border-radius: 10px; 42 | } 43 | -------------------------------------------------------------------------------- /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/SpotComponent/SpotEditComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { format } from "date-fns"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { useParams, useHistory } from "react-router-dom" 5 | import { deleteSpot } from "../../store/spot"; 6 | 7 | 8 | 9 | const SpotEditForm = ({ spot }) => { 10 | const dispatch = useDispatch(); 11 | const history = useHistory(); 12 | 13 | const [errors, setErrors] = useState([]); 14 | 15 | 16 | 17 | const deleteThisSpot = () => { 18 | setErrors([]); 19 | return dispatch(deleteSpot(spot)) 20 | .then(() => { history.push(`/`) }) 21 | .catch((res) => { 22 | if (res.data && res.data.errors) setErrors(res.data.errors); 23 | }) 24 | } 25 | 26 | return ( 27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 | ) 35 | } 36 | 37 | export default SpotEditForm; 38 | -------------------------------------------------------------------------------- /react-app/src/components/User.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 | 40 | ); 41 | } 42 | export default User; 43 | -------------------------------------------------------------------------------- /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/seeds/user_book_spots.py: -------------------------------------------------------------------------------- 1 | from app.models import db, User_Book_Spot, Spot 2 | 3 | def seed_bookings(): 4 | 5 | booking_1 = User_Book_Spot(spots_id=1, guest_id=1, group_size=8) 6 | booked_spot1 = Spot.query.get(booking_1.spots_id) 7 | booked_spot1.availability -= booking_1.group_size 8 | 9 | booking_2 = User_Book_Spot(spots_id=2, guest_id=1, group_size=2) 10 | booked_spot2 = Spot.query.get(booking_2.spots_id) 11 | booked_spot2.availability -= booking_2.group_size 12 | 13 | booking_3 = User_Book_Spot(spots_id=3, guest_id=1, group_size=6) 14 | booked_spot3 = Spot.query.get(booking_3.spots_id) 15 | booked_spot3.availability -= booking_3.group_size 16 | 17 | booking_4 = User_Book_Spot(spots_id=4, guest_id=1, group_size=9) 18 | booked_spot4 = Spot.query.get(booking_4.spots_id) 19 | booked_spot4.availability -= booking_4.group_size 20 | 21 | 22 | db.session.add(booking_1) 23 | db.session.add(booking_2) 24 | db.session.add(booking_3) 25 | db.session.add(booking_4) 26 | db.session.commit() 27 | 28 | def undo_bookings(): 29 | User_Book_Spot.query.delete() 30 | db.session.commit() -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | from .spots import seed_spots, undo_spots 4 | from .user_book_spots import seed_bookings, undo_bookings 5 | from .fundings import seed_fundings, undo_fundings 6 | 7 | # Creates a seed group to hold our commands 8 | # So we can type `flask seed --help` 9 | seed_commands = AppGroup('seed') 10 | 11 | # Creates the `flask seed all` command 12 | 13 | 14 | @seed_commands.command('all') 15 | def seed(): 16 | seed_users() 17 | seed_spots() 18 | seed_bookings() 19 | seed_fundings() 20 | # Add other seed functions here 21 | 22 | 23 | @seed_commands.command('spots') 24 | def seed_spot(): 25 | seed_spots() 26 | 27 | 28 | @seed_commands.command('bookings') 29 | def seed_booking(): 30 | seed_bookings() 31 | 32 | 33 | @seed_commands.command('fundings') 34 | def seed_funding(): 35 | seed_fundings() 36 | # Creates the `flask seed undo` command 37 | 38 | 39 | @seed_commands.command('undo') 40 | def undo(): 41 | undo_fundings() 42 | undo_bookings() 43 | undo_spots() 44 | undo_users() 45 | # Add other undo functions here 46 | -------------------------------------------------------------------------------- /migrations/versions/20210221_133750_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3430e93927f7 4 | Revises: 397ca62cc8d1 5 | Create Date: 2021-02-21 13:37:50.422905 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3430e93927f7' 14 | down_revision = '397ca62cc8d1' 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('fundings', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('user_id', sa.Integer(), nullable=False), 24 | sa.Column('spots_id', sa.Integer(), nullable=False), 25 | sa.Column('sponsorship_cost', sa.Float(), nullable=True), 26 | sa.ForeignKeyConstraint(['spots_id'], ['spots.id'], ), 27 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('fundings') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /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('users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('first_name', sa.String(length=50), nullable=False), 24 | sa.Column('last_name', sa.String(length=50), 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 | ) 30 | # ### end Alembic commands ###qqqqqqqqq 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('users') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/20210221_124947_create_user_book_spot_table.py: -------------------------------------------------------------------------------- 1 | """create_user_book_spot_table 2 | 3 | Revision ID: 397ca62cc8d1 4 | Revises: f0f68448b41c 5 | Create Date: 2021-02-21 12:49:47.799224 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '397ca62cc8d1' 14 | down_revision = 'f0f68448b41c' 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('user_book_spots', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('spots_id', sa.Integer(), nullable=False), 24 | sa.Column('guest_id', sa.Integer(), nullable=False), 25 | sa.Column('group_size', sa.Integer(), nullable=False), 26 | sa.ForeignKeyConstraint(['guest_id'], ['users.id'], ), 27 | sa.ForeignKeyConstraint(['spots_id'], ['spots.id'], ), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('user_book_spots') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /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 (firstName, lastName, 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 | first_name: firstName, 42 | last_name: lastName, 43 | email, 44 | password, 45 | }), 46 | }); 47 | return await response.json(); 48 | } -------------------------------------------------------------------------------- /react-app/src/components/HomePageComponent/newUser.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Alert, Button } from 'react-bootstrap' 3 | 4 | const NewUser = (props) => { 5 | const { show, setShow, setShowLogin, setShowSignUp } = props 6 | 7 | return ( 8 |
9 | 10 | Welcome to CareBnb 11 |

12 | Find your spot to stay at by logging in or signing up today! 13 |

14 |
15 |
16 | 19 | 22 | 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default NewUser -------------------------------------------------------------------------------- /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 | first_name = db.Column(db.String(50), nullable=False) 11 | last_name = db.Column(db.String(50), nullable=False) 12 | email = db.Column(db.String(255), nullable=False, unique=True) 13 | hashed_password = db.Column(db.String(255), nullable=False) 14 | 15 | spots = db.relationship("Spot") 16 | booked_spot = db.relationship("User_Book_Spot", backref="users") 17 | donations = db.relationship("Funding", backref="users") 18 | 19 | @property 20 | def password(self): 21 | return self.hashed_password 22 | 23 | @password.setter 24 | def password(self, password): 25 | self.hashed_password = generate_password_hash(password) 26 | 27 | def check_password(self, password): 28 | return check_password_hash(self.password, password) 29 | 30 | def to_dict(self): 31 | return { 32 | "id": self.id, 33 | "first_name": self.first_name, 34 | "last_name": self.last_name, 35 | "email": self.email 36 | } 37 | -------------------------------------------------------------------------------- /app/forms/spot_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, TextAreaField, IntegerField, SelectField, FloatField 3 | from wtforms.validators import DataRequired 4 | from app.models import Spot 5 | 6 | state = [("AL"), ("AK"), ("AZ"), ("AR"), ("CA"), ("CO"), ("CT"), ("DE"), ("FL"), ("GA"), ("HI"), ("ID"), ("IL"), ("IN"), ("IA"), ("KS"), ("KY"), ("LA"), ("ME"), ("MD"), ("MA"), ("MI"), ("MN"), ("MS"), 7 | ("MO"), ("MT"), ("NE"), ("NV"), ("NH"), ("NJ"), ("NM"), ("NY"), ("NC"), ("ND"), ("OH"), ("OK"), ("OR"), ("PA"), ("RI"), ("SC"), ("SD"), ("TN"), ("TX"), ("UT"), ("VT"), ("VA"), ("WA"), ("WV"), ("WI"), ("WY")] 8 | 9 | 10 | class SpotForm(FlaskForm): 11 | image_url = StringField("spot_image", validators=[DataRequired()]) 12 | title = StringField("title", validators=[DataRequired()]) 13 | address = StringField("address", validators=[DataRequired()]) 14 | city = StringField("city", validators=[DataRequired()]) 15 | state = SelectField("state", choices=state, validators=[DataRequired()]) 16 | zipcode = IntegerField("zipcode", validators=[DataRequired()]) 17 | description = TextAreaField("description", validators=[DataRequired()]) 18 | capacity = IntegerField("capacity", validators=[DataRequired()]) 19 | latitude = FloatField("latitude") 20 | longitude = FloatField("longitude") 21 | -------------------------------------------------------------------------------- /app/api/funding_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import current_user 3 | from app.models import Funding 4 | from app.forms import FundingForm 5 | from app.models.db import db 6 | 7 | funding_routes = Blueprint('fundings', __name__) 8 | 9 | 10 | @funding_routes.route("/") 11 | def all_fundings(): 12 | fundings = Funding.query.filter(Funding.user_id == current_user.id).all() 13 | return {"fundings": {funding.id: funding.to_dict() for funding in fundings}} 14 | 15 | 16 | @funding_routes.route("/", methods=["POST"]) 17 | def create_funding(spot_id): 18 | form = FundingForm() 19 | form['csrf_token'].data = request.cookies['csrf_token'] 20 | if form.validate_on_submit(): 21 | funding = Funding( 22 | user_id=current_user.id, 23 | spots_id=spot_id, 24 | sponsorship_cost=form.data["sponsorship_cost"]) 25 | 26 | db.session.add(funding) 27 | db.session.commit() 28 | return {'funding': {funding.id: funding.to_dict()}} 29 | else: 30 | return "Bad Data" 31 | 32 | # TODO: FIGURE OUT BUG THAT CAUSES FUNDING TO BE NONE 33 | 34 | 35 | @funding_routes.route("/") 36 | def get_specific_fundings(id): 37 | funding = Funding.query.get(id) 38 | # funding = Funding.query.filter(Funding.id == id).first() 39 | return {'funding': {funding.id: funding.to_dict()}} 40 | -------------------------------------------------------------------------------- /react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-native-community/geolocation": "^2.0.2", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "bootstrap": "^4.6.0", 11 | "dotenv": "^8.2.0", 12 | "http-proxy-middleware": "^1.0.5", 13 | "react": "^16.13.1", 14 | "react-aws-s3": "^1.4.0", 15 | "react-bootstrap": "^1.5.0", 16 | "react-bootstrap-icons": "^1.3.0", 17 | "react-dom": "^16.13.1", 18 | "react-geocode": "^0.2.3", 19 | "react-google-maps": "^9.4.5", 20 | "react-redux": "^7.2.2", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "3.4.3", 23 | "redux": "^4.0.5", 24 | "redux-logger": "^3.0.6", 25 | "redux-thunk": "^2.3.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "proxy": "http://localhost:5000" 49 | } 50 | -------------------------------------------------------------------------------- /react-app/src/store/booking.js: -------------------------------------------------------------------------------- 1 | const LOAD = "booking/getAllBookings"; 2 | const ADD_ONE = "booking/addOneBooking"; 3 | 4 | const getAllBookings = (bookings) => { 5 | return { 6 | type: LOAD, 7 | payload: bookings, 8 | }; 9 | }; 10 | 11 | const addOneBooking = (booking) => { 12 | return { 13 | type: ADD_ONE, 14 | payload: booking, 15 | }; 16 | }; 17 | 18 | export const getBookings = () => async (dispatch) => { 19 | const response = await fetch("/api/bookings/"); 20 | const bookings = await response.json(); 21 | return dispatch(getAllBookings(bookings)); 22 | }; 23 | 24 | export const addBooking = (payload) => async (dispatch) => { 25 | const response = await fetch(`/api/bookings/${payload.spotId}`, { 26 | method: "POST", 27 | headers: { "Content-Type": "application/json" }, 28 | body: JSON.stringify(payload), 29 | }); 30 | if (!response.ok) throw response; 31 | const booking = await response.json(); 32 | dispatch(addOneBooking(booking)); 33 | return booking; 34 | }; 35 | 36 | const initialState = {}; 37 | 38 | const bookingReducer = (state = initialState, action) => { 39 | let newState; 40 | switch (action.type) { 41 | case LOAD: 42 | newState = Object.assign({}, state, { ...action.payload }); 43 | return newState; 44 | 45 | case ADD_ONE: 46 | newState = Object.assign({}, state, { 47 | [action.payload.id]: action.payload, 48 | }); 49 | return newState; 50 | default: 51 | return state; 52 | } 53 | }; 54 | 55 | export default bookingReducer; 56 | -------------------------------------------------------------------------------- /react-app/src/components/SearchComponent/SearchPage/search-page.css: -------------------------------------------------------------------------------- 1 | .results-map-container { 2 | display: grid; 3 | grid-template-columns: 50% 50%; 4 | } 5 | 6 | .searched { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | 12 | .search-page-body { 13 | padding: 20px 20px; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .search-controls-container { 19 | display: flex; 20 | justify-content: flex-start; 21 | align-items: center; 22 | width: 100%; 23 | height: 60px; 24 | } 25 | 26 | .search-results-container { 27 | padding: 30px 0px; 28 | display: flex; 29 | justify-content: flex-start; 30 | flex-wrap: wrap; 31 | overflow-y: scroll; 32 | } 33 | 34 | .search-controls-container button { 35 | background-color: #6D696A; 36 | border-color: #6D696A; 37 | } 38 | 39 | .search-controls-container button:hover { 40 | background-color: #6d696acc; 41 | border-color: #6d696acc; 42 | } 43 | 44 | .search-results-container .spotcard { 45 | background-color: #A2A7A5; 46 | padding: 20px; 47 | margin: 10px; 48 | min-width: 300px; 49 | max-width: 300px; 50 | box-shadow: 0 1px 5px #A2A7A5; 51 | transition: all .2s ease-in-out; 52 | } 53 | 54 | .search-results-container .spotcard:hover::after { 55 | opacity: 1; 56 | } 57 | 58 | .search-results-container .spotcard:hover { 59 | transform: scale(1.1, 1.1); 60 | z-index: 2; 61 | } 62 | 63 | .search-results-container::after { 64 | box-shadow: 0 5px 15px #4c4e4d; 65 | opacity: 0; 66 | transition: all .2s ease-in-out; 67 | } -------------------------------------------------------------------------------- /react-app/src/components/FundingComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useParams, Redirect } from "react-router-dom"; 3 | import { useDispatch } from "react-redux"; 4 | import Button from 'react-bootstrap/Button' 5 | import * as fundingActions from "../../store/funding"; 6 | 7 | 8 | const FundingComponent = ({ authenticated }) => { 9 | const dispatch = useDispatch(); 10 | const spotId = Number.parseInt(useParams().spotId); 11 | 12 | const [errors, setErrors] = useState([]); 13 | const [sponsorship_cost, setSponsCost] = useState(""); 14 | 15 | 16 | const updateSponsCost = (e) => { 17 | setSponsCost(e.target.value); 18 | }; 19 | 20 | const onFundingSubmit = async (e) => { 21 | e.preventDefault(); 22 | dispatch(fundingActions.addFunding({spotId, sponsorship_cost})) 23 | // Redirect('/') 24 | } 25 | 26 | 27 | return ( 28 |
29 |
30 | {errors.map((error) => ( 31 |
{error}
32 | ))} 33 |
34 |

Donate

35 | 36 |
37 | 44 |
45 |
46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default FundingComponent; 53 | -------------------------------------------------------------------------------- /app/models/spot.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class Spot(db.Model): 5 | __tablename__ = 'spots' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | image_url = db.Column(db.String(20083), nullable=False) 9 | title = db.Column(db.String(50), nullable=False) 10 | address = db.Column(db.String(50), nullable=False) 11 | city = db.Column(db.String(50), nullable=False) 12 | state = db.Column(db.String(2), nullable=False) 13 | zipcode = db.Column(db.Integer, nullable=False) 14 | description = db.Column(db.String(500), nullable=False) 15 | capacity = db.Column(db.Integer, nullable=False) 16 | availability = db.Column(db.Integer, nullable=False) 17 | latitude = db.Column(db.Float) 18 | longitude = db.Column(db.Float) 19 | host_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) 20 | 21 | host = db.relationship("User") 22 | booking = db.relationship( 23 | "User_Book_Spot", cascade="all, delete", backref="spots") 24 | funds = db.relationship("Funding", cascade="all, delete", backref="spots") 25 | 26 | def to_dict(self): 27 | return { 28 | "id": self.id, 29 | "image_url": self.image_url, 30 | "title": self.title, 31 | "address": self.address, 32 | "city": self.city, 33 | "state": self.state, 34 | "zipcode": self.zipcode, 35 | "description": self.description, 36 | "capacity": self.capacity, 37 | "availability": self.availability, 38 | "latitude": self.latitude, 39 | "longitude": self.longitude, 40 | "host_id": self.host_id 41 | } 42 | -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotViewMini/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Card } from 'react-bootstrap'; 3 | import { useHistory } from 'react-router-dom' 4 | import 'bootstrap/dist/css/bootstrap.min.css' 5 | import './SpotViewMini.css' 6 | 7 | const SpotViewMini = ({ spot }) => { 8 | const history = useHistory(); 9 | // const test_spot = { 10 | // image_url:'https://i.ytimg.com/vi/tiZ8u6PNTK8/maxresdefault.jpg', 11 | // image_url2:'https://upload.wikimedia.org/wikipedia/commons/1/15/Mount_Massive.jpg', 12 | // title:"Steve's house", 13 | // description:'If you need a place to stay, come crash at my dig! the furniture is a bit blocky but I will feed you steak', 14 | // city:'Atlantis', 15 | // state:'Georgia', 16 | // availability:5 17 | // } 18 | 19 | return ( 20 | //
21 | // {/* Spot Image */} 22 | //

{spot.description}

23 | //

{spot.title}

24 | //

25 | // {spot.city}, {spot.state} 26 | //

27 | //

{spot.availability}

28 | //
29 | 30 | 31 | 32 |

33 | {spot.title} 34 | {spot.description} 35 | 36 |
    {spot.city}, {spot.state}
37 |
    availability: {spot.availability}
38 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default SpotViewMini; 45 | -------------------------------------------------------------------------------- /migrations/versions/20210220_134806_create_spots_table.py: -------------------------------------------------------------------------------- 1 | """create_spots_table 2 | 3 | Revision ID: 2843224166c6 4 | Revises: ffdc0a98111c 5 | Create Date: 2021-02-20 13:48:06.306597 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '2843224166c6' 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('spots', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('image_url', sa.String( 24 | length=20083), nullable=False), 25 | sa.Column('title', sa.String(length=50), nullable=False), 26 | sa.Column('address', sa.String(length=50), nullable=False), 27 | sa.Column('city', sa.String(length=50), nullable=False), 28 | sa.Column('state', sa.String(length=2), nullable=False), 29 | sa.Column('description', sa.String( 30 | length=500), nullable=False), 31 | sa.Column('capacity', sa.Integer(), nullable=False), 32 | sa.Column('availability', sa.Integer(), nullable=False), 33 | sa.Column('host_id', sa.Integer(), nullable=False), 34 | sa.ForeignKeyConstraint(['host_id'], ['users.id'], ), 35 | sa.PrimaryKeyConstraint('id') 36 | ) 37 | # ### end Alembic commands ### 38 | 39 | 40 | def downgrade(): 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_table('spots') 43 | # ### end Alembic commands ### 44 | -------------------------------------------------------------------------------- /react-app/src/components/BookingPageComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useParams, Redirect, useHistory } from "react-router-dom"; 3 | import { useDispatch } from "react-redux"; 4 | import Button from 'react-bootstrap/Button' 5 | import * as bookingActions from "../../store/booking"; 6 | 7 | 8 | const BookingPageComponent = ({ authenticated, spotTitle, spotAvailability }) => { 9 | const dispatch = useDispatch(); 10 | const history = useHistory() 11 | const spotId = Number.parseInt(useParams().spotId); 12 | 13 | const [errors, setErrors] = useState([]); 14 | const [group_size, setGroupsize] = useState(""); 15 | 16 | 17 | const updateGroupsize = (e) => { 18 | setGroupsize(e.target.value); 19 | }; 20 | 21 | const onBookingSubmit = async (e) => { 22 | e.preventDefault(); 23 | 24 | dispatch(bookingActions.addBooking({spotId, group_size})) 25 | window.alert(`Thank you for booking your stay at, ${spotTitle}!`) 26 | history.push("/") 27 | } 28 | 29 | 30 | return ( 31 |
32 |
33 | {errors.map((error) => ( 34 |
{error}
35 | ))} 36 |
37 |

Book your stay

38 | 39 |
40 | 47 |
48 | 49 |
50 | {spotAvailability <= 0 && 51 |

No more spots

52 | } 53 | {spotAvailability > 0 && 54 | 55 | } 56 |
57 | 58 |
59 | ); 60 | }; 61 | 62 | export default BookingPageComponent; 63 | -------------------------------------------------------------------------------- /app/api/booking_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import current_user 3 | from ..forms import BookingForm 4 | from app.models import User_Book_Spot, Spot 5 | from app.models.db import db 6 | 7 | 8 | booking_routes = Blueprint('bookings', __name__) 9 | 10 | 11 | @booking_routes.route("/") 12 | def all_bookings(): 13 | bookings = User_Book_Spot.query.all() 14 | return {"all_bookings": {booking.id: booking.to_dict() for booking in bookings}} 15 | 16 | 17 | @booking_routes.route("/") 18 | def single_booking(id): 19 | booking = User_Book_Spot.query.get(id) 20 | return {"booking": booking.to_dict()} 21 | 22 | 23 | @booking_routes.route("/", methods=["POST"]) 24 | def add_booking(spot_id): 25 | form = BookingForm() 26 | form['csrf_token'].data = request.cookies['csrf_token'] 27 | if form.validate_on_submit(): 28 | spot = Spot.query.get(spot_id) 29 | spot.availability -= form.data["group_size"] 30 | # logic to prevent availability under zero to book 31 | # if spot.availability <= 0: 32 | # return "Spot is fully booked" 33 | booking = User_Book_Spot( 34 | spots_id=spot_id, 35 | guest_id=current_user.id, 36 | group_size=form.data["group_size"]) 37 | 38 | db.session.add(booking) 39 | db.session.commit() 40 | return {'booking': {booking.id: booking.to_dict()}} 41 | else: 42 | return "Bad Data" 43 | 44 | 45 | @booking_routes.route('/', methods=['DELETE']) 46 | def delete_booking(id): 47 | booking = User_Book_Spot.query.filter(User_Book_Spot.id == id).first() 48 | db.session.delete(booking) 49 | db.session.commit() 50 | return {'deleted': True} 51 | 52 | # @spot_routes.route('/', methods=["DELETE"]) 53 | # def delete_spot(id): 54 | # spot = Spot.query.filter(Spot.id == id).first() 55 | # db.session.delete(spot) 56 | # db.session.commit() 57 | # return {'deleted': True} 58 | -------------------------------------------------------------------------------- /react-app/src/components/UsersList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch } from 'react-redux'; 3 | import { NavLink } from "react-router-dom"; 4 | import { createSpot, deleteSpot, getSpots } from "../store/spot"; 5 | 6 | function UsersList() { 7 | const dispatch = useDispatch() 8 | const [users, setUsers] = useState([]); 9 | 10 | useEffect(() => { 11 | async function fetchData() { 12 | const response = await fetch("/api/users/"); 13 | const responseData = await response.json(); 14 | setUsers(responseData.users); 15 | } 16 | fetchData(); 17 | }, []); 18 | 19 | useEffect(() => { 20 | dispatch(getSpots()) 21 | }, [dispatch]) 22 | 23 | // import + dispatch any thunk action in here to debug/test 24 | const testFunc = () => { 25 | // const image_url = 'www.test.com' 26 | // const title = 'Test 3spot title' 27 | // const address = 'Test 3spot address' 28 | // const city = 'Test 3city' 29 | // const state = 'TX' 30 | // const zipcode = 76107 31 | // const description = 'Test 3description' 32 | // const capacity = 400 33 | // const availability = 400 34 | 35 | // const payload = { 36 | // image_url, 37 | // title, 38 | // address, 39 | // city, 40 | // state, 41 | // zipcode, 42 | // description, 43 | // capacity, 44 | // availability, 45 | // } 46 | // dispatch(createSpot(payload)) 47 | 48 | dispatch(deleteSpot({ spotId: 4 })) 49 | 50 | } 51 | 52 | const userComponents = users.map((user) => { 53 | return ( 54 |
  • 55 | {user.first_name} 56 |
  • 57 | ); 58 | }); 59 | 60 | return ( 61 | <> 62 | {/* button below is for testing our redux store */} 63 | 64 |

    User List:

    65 |
      {userComponents}
    66 | 67 | ); 68 | } 69 | 70 | export default UsersList; 71 | -------------------------------------------------------------------------------- /react-app/src/components/SearchComponent/SearchPage/searchmap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withScriptjs, withGoogleMap, GoogleMap, Marker, InfoWindow } from "react-google-maps" 3 | import { NavLink } from 'react-router-dom'; 4 | import SpotViewMini from '../../SpotComponent/SpotViewMini'; 5 | import mapStyles from '../../GoogleMapsComponent/mapStyles'; 6 | 7 | const MySearchMap = withScriptjs(withGoogleMap((props) => { 8 | const { position, allSpots, selectedPark, setSelectedPark } = props; 9 | 10 | 11 | return ( 12 | 18 | {allSpots && 19 | Object.values(allSpots).map((spot) => ( 20 | { 24 | setSelectedPark(spot); 25 | }} 26 | icon={{ 27 | url: "https://i.postimg.cc/gjTZdbW3/CBlogo-Transparent.png", 28 | scaledSize: new window.google.maps.Size(35, 35), 29 | }} 30 | /> 31 | ))} 32 | {selectedPark && ( 33 | { 36 | setSelectedPark(null); 37 | }} 38 | > 39 | 40 | 41 | 42 | 43 | )} 44 | 45 | ) 46 | })); 47 | 48 | export default MySearchMap; -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotPage/SpotPage.css: -------------------------------------------------------------------------------- 1 | .spot-body { 2 | /* background-color: aqua; */ 3 | /* display: flex; 4 | flex-direction: row; */ 5 | /* border: solid black 2px; */ 6 | margin: 20px; 7 | background-color: #dae2df; 8 | border-radius: 10px; 9 | } 10 | 11 | .spot-image-and-info { 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: space-between; 15 | /* height: 50%; */ 16 | } 17 | 18 | @media(max-width:1015px) { 19 | .spot-image-and-info { 20 | flex-direction: column; 21 | align-items: center; 22 | } 23 | } 24 | 25 | .spot-image { 26 | display: flex; 27 | justify-content: center; 28 | /* max-width: 800px; 29 | max-height: 800px; */ 30 | margin: auto; 31 | padding: 10px 10px; 32 | } 33 | 34 | .spot-image img { 35 | width: 600px; 36 | height: 450px; 37 | object-fit: cover; 38 | border-radius: 10px; 39 | } 40 | 41 | .spot-info { 42 | /* border: solid red 2px; */ 43 | width: 60%; 44 | /* display: flex; */ 45 | } 46 | 47 | .spot-info-div { 48 | margin-top: 55px;; 49 | } 50 | 51 | .spot-info h3 { 52 | margin-left: 110px; 53 | } 54 | 55 | .spot-donate-and-book { 56 | /* border: solid black 2px; */ 57 | display: flex; 58 | flex-direction: row; 59 | justify-content: space-around; 60 | margin-top: 5rem; 61 | } 62 | 63 | .spot-donate { 64 | /* border: solid red 2px; */ 65 | width: 48%; 66 | background-color: #a2a7a5; 67 | /* border: solid black 2px; */ 68 | border-bottom-left-radius: 10px; 69 | border-top-left-radius: 10px; 70 | margin-bottom: 55px; 71 | padding: 5px; 72 | } 73 | 74 | .spot-book { 75 | /* border: solid blue 2px; */ 76 | width: 48%; 77 | background-color: #a2a7a5; 78 | /* border: solid black 2px; */ 79 | /* margin-left: 30px; */ 80 | border-bottom-right-radius: 10px; 81 | border-top-right-radius: 10px; 82 | margin-bottom: 55px; 83 | padding: 5px; 84 | } 85 | 86 | .spot-hr { 87 | width: 80%; 88 | } -------------------------------------------------------------------------------- /react-app/src/store/funding.js: -------------------------------------------------------------------------------- 1 | const LOAD = "funding/getAllFundings"; 2 | const LOAD_ONE = "funding/getOneFunding"; 3 | const ADD_ONE = "funding/addOneFunding"; 4 | 5 | const getAllFundings = (fundings) => { 6 | return { 7 | type: LOAD, 8 | payload: fundings, 9 | }; 10 | }; 11 | 12 | const getOneFunding = (funding) => { 13 | return { 14 | type: LOAD_ONE, 15 | payload: funding, 16 | }; 17 | }; 18 | 19 | const addOneFunding = (funding) => { 20 | return { 21 | type: ADD_ONE, 22 | payload: funding, 23 | }; 24 | }; 25 | 26 | export const getFundings = () => async (dispatch) => { 27 | const response = await fetch("api/fundings/"); 28 | if (!response.ok) throw response; 29 | const fundings = await response.json(); 30 | return dispatch(getAllFundings(fundings)); 31 | }; 32 | 33 | export const getFunding = (id) => async (dispatch) => { 34 | const response = await fetch(`api/fundings/${id}`); 35 | if (!response.ok) throw response; 36 | const funding = await response.json(); 37 | return dispatch(getOneFunding(funding)); 38 | }; 39 | 40 | export const addFunding = (payload) => async (dispatch) => { 41 | const response = await fetch(`/api/fundings/${payload.spotId}`, { 42 | method: "POST", 43 | headers: { "Content-Type": "application/json" }, 44 | body: JSON.stringify(payload), 45 | }); 46 | if (!response.ok) throw response; 47 | const funding = await response.json(); 48 | dispatch(addOneFunding(funding)); 49 | return funding; 50 | }; 51 | 52 | const inititalState = {}; 53 | 54 | const fundingReducer = (state = inititalState, action) => { 55 | let newState; 56 | switch (action.type) { 57 | case LOAD: 58 | newState = Object.assign({}, state, { ...action.payload }); 59 | return newState; 60 | case LOAD_ONE: 61 | newState = Object.assign({}, state, { ...action.payload }); 62 | return newState; 63 | case ADD_ONE: 64 | newState = Object.assign({}, state, { 65 | [action.payload.id]: action.payload, 66 | }); 67 | return newState; 68 | default: 69 | return state; 70 | } 71 | }; 72 | 73 | export default fundingReducer; 74 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash 2 | from app.models import db, User 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | 6 | 7 | def seed_users(): 8 | 9 | demo = User(first_name='Demo', last_name='User', email='demo@aa.io', 10 | password='password') 11 | 12 | gen = User(first_name='Gen', last_name='Ohta', email='gen@aa.com', 13 | password='genpw') 14 | 15 | juliet = User(first_name='Juliet', last_name='Shafto', email='juliet@aa.com', 16 | password='julietpw') 17 | 18 | alfredo = User(first_name='Alfredo', last_name='Quiroga', email='alfredo@aa.com', 19 | password='alfredopw') 20 | 21 | ed = User(first_name='Ed', last_name='Herman', email='ed@aa.com', 22 | password='edpw') 23 | 24 | john = User(first_name='John', last_name='Tipton', email='tipton@gmail.com', 25 | password='password') 26 | 27 | adam = User(first_name='Adam', last_name='Burton', email='burton@gmail.com', 28 | password='password') 29 | 30 | frank = User(first_name='Frank', last_name='Clinton', email='fclint@gmail.com', 31 | password='password') 32 | 33 | claire = User(first_name='Claire', last_name='Costano', email='claircos@gmail.com', 34 | password='password') 35 | 36 | monica = User(first_name='Monica', last_name='Smith', email='msmith@gmail.com', 37 | password='password') 38 | 39 | db.session.add(demo) 40 | db.session.add(gen) 41 | db.session.add(juliet) 42 | db.session.add(alfredo) 43 | db.session.add(john) 44 | db.session.add(adam) 45 | db.session.add(frank) 46 | db.session.add(claire) 47 | db.session.add(monica) 48 | 49 | db.session.commit() 50 | 51 | # Uses a raw SQL query to TRUNCATE the users table. 52 | # SQLAlchemy doesn't have a built in function to do this 53 | # TRUNCATE Removes all the data from the table, and resets 54 | # the auto incrementing primary key 55 | 56 | 57 | def undo_users(): 58 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;') 59 | db.session.commit() 60 | -------------------------------------------------------------------------------- /react-app/src/components/SearchComponent/SearchBar/search-bar.css: -------------------------------------------------------------------------------- 1 | .search-bar-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | 6 | 7 | background-color: #ffff; 8 | border-radius: 2em; 9 | padding-top: 12px; 10 | min-width: 800px; 11 | box-shadow: 0 1px 2px #A2A7A5; 12 | position: relative; 13 | } 14 | 15 | .search-bar-container::after { 16 | box-shadow: 0 5px 15px #4c4e4d; 17 | opacity: 0; 18 | transition: opacity 0.3s ease-in-out; 19 | } 20 | 21 | .search-bar-container:hover::after { 22 | opacity: 1; 23 | } 24 | 25 | .search-bar-container:hover { 26 | transform: scale(1.1, 1.1); 27 | } 28 | 29 | .search-bar-form svg { 30 | font-size: 35px; 31 | } 32 | 33 | 34 | .search-bar-form label { 35 | margin-top: 7px; 36 | margin-left: 10px; 37 | } 38 | 39 | .search-bar-form { 40 | display: flex; 41 | flex-direction: row; 42 | text-align: center; 43 | } 44 | 45 | .guest-input.input-group { 46 | height: 35px; 47 | width: 50px; 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | margin: 0px 10px; 52 | } 53 | 54 | .city-input.input-group { 55 | height: 35px; 56 | width: 300px; 57 | display: flex; 58 | align-items: center; 59 | justify-content: center; 60 | padding: 0px 10px; 61 | 62 | 63 | } 64 | 65 | .city-input .form-control { 66 | background-color: transparent; 67 | outline: none; 68 | outline-style: none; 69 | box-shadow: none; 70 | border: 0; 71 | border-radius: 0; 72 | border-bottom: solid #3a3838 2px; 73 | text-align: center; 74 | } 75 | 76 | input::-webkit-outer-spin-button, 77 | input::-webkit-inner-spin-button { 78 | -webkit-appearance: none; 79 | margin: 0; 80 | } 81 | 82 | .guest-input .form-control { 83 | background-color: transparent; 84 | outline: none; 85 | outline-style: none; 86 | box-shadow: none; 87 | border: 0; 88 | border-radius: 0; 89 | border-bottom: solid #3a3838 2px; 90 | text-align: center; 91 | } 92 | 93 | .btn { 94 | height: calc(1.5em + .75rem + 2px); 95 | margin: 0px 10px; 96 | } -------------------------------------------------------------------------------- /react-app/src/components/GoogleMapsComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import SpotViewMini from "../SpotComponent/SpotViewMini"; 4 | import { 5 | GoogleMap, 6 | withScriptjs, 7 | withGoogleMap, 8 | Marker, 9 | InfoWindow, 10 | } from "react-google-maps"; 11 | import Geolocation from "@react-native-community/geolocation"; 12 | import mapStyles from "./mapStyles"; 13 | import { getSpots } from "../../store/spot"; 14 | import { NavLink } from "react-router-dom"; 15 | import "./GoogleMap.css" 16 | 17 | const GoogleMapsComponent = () => { 18 | const allSpots = useSelector((state) => state.spots.all_spots); 19 | const dispatch = useDispatch(); 20 | const [selectedPark, setSelectedPark] = useState(null); 21 | 22 | useEffect(() => { 23 | dispatch(getSpots()); 24 | }, [dispatch]); 25 | 26 | const image = "react-app/src/components/GoogleMapsComponent/marker_logo.png"; 27 | 28 | return ( 29 | 34 | {allSpots && 35 | Object.values(allSpots).map((spot) => ( 36 | { 40 | setSelectedPark(spot); 41 | }} 42 | icon={{ 43 | url: "https://i.postimg.cc/gjTZdbW3/CBlogo-Transparent.png", 44 | scaledSize: new window.google.maps.Size(35, 35), 45 | }} 46 | /> 47 | ))} 48 | {selectedPark && ( 49 | { 52 | setSelectedPark(null); 53 | }} 54 | > 55 | 56 | 57 | 58 | 59 | )} 60 | 61 | ); 62 | }; 63 | 64 | export const WrappedGoogleMap = withScriptjs( 65 | withGoogleMap(GoogleMapsComponent) 66 | ); -------------------------------------------------------------------------------- /app/api/spot_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import current_user 3 | from app.models import db, Spot 4 | from app.forms import SpotForm 5 | from sqlalchemy import and_ 6 | 7 | spot_routes = Blueprint('spots', __name__) 8 | 9 | 10 | @spot_routes.route('/') 11 | def all_spots(): 12 | spots = Spot.query.all() 13 | return {"all_spots": {spot.id: spot.to_dict() for spot in spots}} 14 | 15 | 16 | @spot_routes.route('/', methods=["POST"]) 17 | def create_spot(): 18 | form = SpotForm() 19 | form['csrf_token'].data = request.cookies['csrf_token'] 20 | if form.validate_on_submit(): 21 | spot = Spot( 22 | image_url=form.data['image_url'], 23 | title=form.data['title'], 24 | address=form.data['address'], 25 | city=form.data['city'], 26 | state=form.data['state'], 27 | zipcode=form.data['zipcode'], 28 | description=form.data['description'], 29 | capacity=form.data['capacity'], 30 | availability=form.data['capacity'], 31 | latitude=form.data["latitude"], 32 | longitude=form.data["longitude"], 33 | host_id=current_user.id, 34 | ) 35 | db.session.add(spot) 36 | db.session.commit() 37 | return {"spot": spot.to_dict()} 38 | return {"errors": "set errors here"} 39 | 40 | 41 | @spot_routes.route('/', methods=["DELETE"]) 42 | def delete_spot(id): 43 | spot = Spot.query.filter(Spot.id == id).first() 44 | db.session.delete(spot) 45 | db.session.commit() 46 | return {'spot': spot.to_dict()} 47 | 48 | 49 | @spot_routes.route('/') 50 | def single_spot(id): 51 | spot = Spot.query.get(id) 52 | return {'spot': spot.to_dict()} 53 | 54 | 55 | @spot_routes.route('/top-available') 56 | def available_spots(): 57 | spots = Spot.query.filter(Spot.availability > 0).order_by(Spot.availability.desc()).limit(10) 58 | return {'available_spots': [spot.to_dict() for spot in spots]} 59 | 60 | 61 | @spot_routes.route('/search/&') 62 | def search_spots(guest_size, city): 63 | spots = Spot.query.filter( 64 | and_(Spot.city.ilike(f'{city}%'), Spot.availability >= guest_size)) 65 | return {'searched_spots': [spot.to_dict() for spot in spots]} 66 | -------------------------------------------------------------------------------- /react-app/src/components/GoogleMapsComponent/mapStyles.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | featureType: "administrative", 4 | elementType: "labels.text.fill", 5 | stylers: [ 6 | { 7 | color: "#444444", 8 | }, 9 | ], 10 | }, 11 | { 12 | featureType: "landscape", 13 | elementType: "all", 14 | stylers: [ 15 | { 16 | color: "#f2f2f2", 17 | }, 18 | ], 19 | }, 20 | { 21 | featureType: "landscape", 22 | elementType: "geometry.fill", 23 | stylers: [ 24 | { 25 | color: "#dae2df", 26 | }, 27 | ], 28 | }, 29 | { 30 | featureType: "poi", 31 | elementType: "all", 32 | stylers: [ 33 | { 34 | visibility: "off", 35 | }, 36 | ], 37 | }, 38 | { 39 | featureType: "road", 40 | elementType: "all", 41 | stylers: [ 42 | { 43 | saturation: -100, 44 | }, 45 | { 46 | lightness: 45, 47 | }, 48 | ], 49 | }, 50 | { 51 | featureType: "road.highway", 52 | elementType: "all", 53 | stylers: [ 54 | { 55 | visibility: "simplified", 56 | }, 57 | ], 58 | }, 59 | { 60 | featureType: "road.highway", 61 | elementType: "geometry.fill", 62 | stylers: [ 63 | { 64 | visibility: "on", 65 | }, 66 | { 67 | color: "#6d696a", 68 | }, 69 | ], 70 | }, 71 | { 72 | featureType: "road.arterial", 73 | elementType: "geometry.fill", 74 | stylers: [ 75 | { 76 | color: "#a2a7a5", 77 | }, 78 | ], 79 | }, 80 | { 81 | featureType: "road.arterial", 82 | elementType: "labels.icon", 83 | stylers: [ 84 | { 85 | visibility: "off", 86 | }, 87 | ], 88 | }, 89 | { 90 | featureType: "transit", 91 | elementType: "all", 92 | stylers: [ 93 | { 94 | visibility: "off", 95 | }, 96 | ], 97 | }, 98 | { 99 | featureType: "water", 100 | elementType: "all", 101 | stylers: [ 102 | { 103 | color: "#46bcec", 104 | }, 105 | { 106 | visibility: "on", 107 | }, 108 | ], 109 | }, 110 | { 111 | featureType: "water", 112 | elementType: "geometry.fill", 113 | stylers: [ 114 | { 115 | color: "#e2dadb", 116 | }, 117 | ], 118 | }, 119 | ]; 120 | -------------------------------------------------------------------------------- /react-app/src/components/FooterComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import "./FooterComponent.css" 3 | 4 | const FooterComponent = () => { 5 | 6 | const imagePath = process.env.NODE_ENV === "production" ? "/static" : ""; 7 | 8 | return ( 9 |
    10 | 18 | 26 |
    CareBnB Devs
    27 | 35 | 43 |
    44 | ) 45 | } 46 | 47 | export default FooterComponent -------------------------------------------------------------------------------- /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.spot_routes import spot_routes 12 | from .api.booking_routes import booking_routes 13 | from .api.funding_routes import funding_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(spot_routes, url_prefix='/api/spots') 38 | app.register_blueprint(booking_routes, url_prefix='/api/bookings') 39 | app.register_blueprint(funding_routes, url_prefix='/api/fundings') 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 | if path == 'favicon.ico': 78 | return app.send_static_file('favicon.ico') 79 | return app.send_static_file('index.html') 80 | -------------------------------------------------------------------------------- /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']}, 401 29 | 30 | 31 | @auth_routes.route('/login', methods=['POST']) 32 | def login(): 33 | """ 34 | Logs a user in 35 | """ 36 | form = LoginForm() 37 | # Get the csrf_token from the request cookie and put it into the 38 | # form manually to validate_on_submit can be used 39 | form['csrf_token'].data = request.cookies['csrf_token'] 40 | if form.validate_on_submit(): 41 | # Add the user to the session, we are logged in! 42 | user = User.query.filter(User.email == form.data['email']).first() 43 | login_user(user) 44 | return user.to_dict() 45 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 46 | 47 | 48 | @auth_routes.route('/logout') 49 | def logout(): 50 | """ 51 | Logs a user out 52 | """ 53 | logout_user() 54 | return {'message': 'User logged out'} 55 | 56 | 57 | @auth_routes.route('/signup', methods=['POST']) 58 | def sign_up(): 59 | """ 60 | Creates a new user and logs them in 61 | """ 62 | form = SignUpForm() 63 | form['csrf_token'].data = request.cookies['csrf_token'] 64 | if form.validate_on_submit(): 65 | user = User( 66 | first_name=form.data['first_name'], 67 | last_name=form.data['last_name'], 68 | email=form.data['email'], 69 | password=form.data['password'] 70 | ) 71 | db.session.add(user) 72 | db.session.commit() 73 | login_user(user) 74 | 75 | return user.to_dict() 76 | return {'errors': validation_errors_to_error_messages(form.errors)} 77 | 78 | 79 | @auth_routes.route('/unauthorized') 80 | def unauthorized(): 81 | """ 82 | Returns unauthorized JSON when flask-login authentication fails 83 | """ 84 | return {'errors': ['Unauthorized']}, 401 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | keys.js 140 | apikey.js -------------------------------------------------------------------------------- /react-app/src/components/auth/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import { login } from "../../services/auth"; 4 | import { Modal, Button } from "react-bootstrap" 5 | 6 | 7 | const LoginModal = (props) => { 8 | const [errors, setErrors] = useState([]); 9 | const [email, setEmail] = useState(""); 10 | const [password, setPassword] = useState(""); 11 | const { authenticated, setAuthenticated } = props 12 | 13 | const switchToSignup = () => { 14 | props.setShowLogin(false); 15 | props.setShowSignUp(true); 16 | }; 17 | 18 | const onLogin = async (e) => { 19 | e.preventDefault(); 20 | const user = await login(email, password); 21 | if (!user.errors) { 22 | setAuthenticated(true); 23 | } else { 24 | setErrors(user.errors); 25 | props.setShowLogin(true) 26 | } 27 | }; 28 | 29 | const onDemoLogin = async (e) => { 30 | e.preventDefault(); 31 | const user = await login('demo@aa.io', 'password'); 32 | if (!user.errors) { 33 | setAuthenticated(true); 34 | } else { 35 | setErrors(user.errors); 36 | } 37 | }; 38 | 39 | const updateEmail = (e) => { 40 | setEmail(e.target.value); 41 | }; 42 | 43 | const updatePassword = (e) => { 44 | setPassword(e.target.value); 45 | }; 46 | 47 | if (authenticated) { 48 | return ; 49 | } 50 | 51 | return ( 52 | 58 | 59 | 60 | Login 61 | 62 | 63 | 64 |
    65 |
    66 | {errors.map((error) => ( 67 |
    {error}
    68 | ))} 69 |
    70 |
    71 | 72 | 79 |
    80 |
    81 | 82 | 89 |
    90 |
    91 | 92 | 93 | 94 |
    95 |
    96 |
    97 | 98 | 99 | 100 |
    101 | ); 102 | } 103 | 104 | export default LoginModal -------------------------------------------------------------------------------- /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 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 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/SearchComponent/SearchPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import queryString from 'query-string'; 4 | import Button from 'react-bootstrap/Button' 5 | import { ArrowDownCircle } from 'react-bootstrap-icons' 6 | import { useLocation } from 'react-router-dom'; 7 | 8 | import { googleApiKey } from '../../GoogleMapsComponent/apikey' 9 | import SpotViewMini from '../../SpotComponent/SpotViewMini' 10 | import SearchBar from '../SearchBar' 11 | import { searchSpots } from '../../../store/spot'; 12 | import MySearchMap from './searchmap'; 13 | import './search-page.css'; 14 | 15 | const SearchPage = () => { 16 | const dispatch = useDispatch() 17 | const { search } = useLocation() 18 | const { city, guest } = queryString.parse(search) 19 | 20 | const [selectedSpot, setSelectedSpot] = useState(0) 21 | const [selectedPark, setSelectedPark] = useState(null) 22 | 23 | const spots_state = useSelector(state => state.spots.searched_spots) 24 | 25 | 26 | useEffect(() => { 27 | dispatch(searchSpots(guest, city)) 28 | }, [dispatch]) 29 | 30 | const handleSearch = (spots_state) => { 31 | let spot_obj = [] 32 | for (let spot in spots_state) { 33 | spot_obj.push(spots_state[spot]) 34 | } 35 | return spot_obj.map((spot, idx) => ( 36 |
    37 | 38 | 39 |
    40 | )) 41 | } 42 | 43 | const grammar = (amount) => { 44 | if (amount == 1) return 'person' 45 | return 'people' 46 | } 47 | 48 | const downCirc = () => { 49 | return () 50 | } 51 | 52 | return ( 53 |
    54 |
    55 | {

    Spots available to book in '{city}' for {guest} {grammar(guest)}

    } 56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 | {spots_state && spots_state.length === 0 &&

    Please alter your search

    } 64 | {handleSearch(spots_state)} 65 |
    66 |
    67 | {spots_state && } 70 | containerElement={
    } 71 | mapElement={
    } 72 | position={spots_state[selectedSpot] ? { lat: spots_state[selectedSpot].latitude, lng: spots_state[selectedSpot].longitude } : { lat: 20, lng: 20 }} 73 | allSpots={spots_state} 74 | selectedPark={selectedPark} 75 | setSelectedPark={setSelectedPark} 76 | />} 77 |
    78 |
    79 | 80 |
    81 | ) 82 | }; 83 | 84 | export default SearchPage -------------------------------------------------------------------------------- /react-app/src/components/HomePageComponent/home-page.css: -------------------------------------------------------------------------------- 1 | .welcome-image { 2 | object-fit: cover; 3 | height: 500px; 4 | width: 100%; 5 | /* filter: blur(1px); */ 6 | } 7 | 8 | .home-search { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | /* position: sticky; */ 13 | /* top: 15px; */ 14 | /* z-index: 1030; */ 15 | background: 0; 16 | /* width: 80%; */ 17 | } 18 | 19 | /* @media screen and (max-width: 1360px) { 20 | .home-search { 21 | position: fixed; 22 | z-index: 2; 23 | top: 90px; 24 | width: 100%; 25 | } 26 | 27 | .search-bar-container { 28 | width: 100%; 29 | border-radius: 0; 30 | background-color: #E2DADB; 31 | } 32 | } */ 33 | 34 | .search-container { 35 | /* display: relative; */ 36 | top: -500px; 37 | } 38 | 39 | #namaste { 40 | height: 100px; 41 | } 42 | 43 | #carouselitem { 44 | height: 300px; 45 | text-align: center; 46 | padding-top: 40px; 47 | } 48 | 49 | 50 | .carousel-caption p, .carousel-control-prev, .carousel-control-next { 51 | color: #A2A7A5 52 | } 53 | 54 | .picture-color { 55 | position: relative; 56 | top: -80px; 57 | width: 100%; 58 | height: 80px; 59 | background-color: #E2DADB; 60 | } 61 | 62 | #mission-statement { 63 | font-size: 35px; 64 | font-family: 'Josefin Sans'; 65 | } 66 | 67 | .spot-view-large-body { 68 | background-color: lightgray; 69 | transition: box-shadow .3s; 70 | height: 550px; 71 | min-width: 400px; 72 | width: 400px; 73 | margin: 20px 20px; 74 | padding: 20px; 75 | border-radius: 0.5em; 76 | } 77 | 78 | .spot-view-large-body:hover { 79 | box-shadow: 0 0 11px rgba(0, 0, 0, 0.26); 80 | } 81 | 82 | .home-body { 83 | width: 100%; 84 | height: 100%; 85 | display: flex; 86 | flex-direction: column; 87 | align-items: center; 88 | position: relative; 89 | top: -33px; 90 | } 91 | 92 | .home-search-bar-container { 93 | display: flex; 94 | flex-direction: column; 95 | justify-content: center; 96 | align-items: center; 97 | height: 600px; 98 | width: 100%; 99 | } 100 | 101 | .welcome-search-container { 102 | height: 500px; 103 | width: 100%; 104 | position: relative; 105 | top: -30px; 106 | } 107 | 108 | 109 | .newest-spots-container { 110 | width: 100%; 111 | display: flex; 112 | flex-wrap: nowrap; 113 | overflow-x: scroll; 114 | margin: 50px 20px; 115 | } 116 | .newest-spots-container .spotcard { 117 | background-color: #DAE2DF; 118 | padding: 20px; 119 | margin: 10px; 120 | min-width: 300px; 121 | max-width: 300px; 122 | box-shadow: 0 1px 5px #A2A7A5; 123 | transition: all .2s ease-in-out; 124 | } 125 | 126 | 127 | .mission-container { 128 | display: grid; 129 | grid-template-columns: 70% 30%; 130 | height: 300px; 131 | width: 100%; 132 | } 133 | 134 | .mission-statement-container { 135 | display: flex; 136 | flex-direction: column; 137 | justify-content: center; 138 | align-items: center; 139 | padding: 0px 50px; 140 | } 141 | 142 | .amount-sheltered-container { 143 | /* background-color: #DAE2DF; */ 144 | /* display: flex; */ 145 | /* flex-direction: column; 146 | justify-content: center; 147 | align-items: center; */ 148 | } 149 | 150 | .google-maps-container { 151 | height: 500px; 152 | width: 100%; 153 | background-color: lightslategrey; 154 | } -------------------------------------------------------------------------------- /react-app/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { Navbar, Nav, Button } from "react-bootstrap"; 4 | 5 | import LogoutButton from "./auth/LogoutButton"; 6 | 7 | const NavBar = ({ authenticated, setAuthenticated, setShowLogin, setShowSignUp }) => { 8 | 9 | const history = useHistory(); 10 | 11 | const hostASpot = (e) => { 12 | e.preventDefault(); 13 | 14 | if (authenticated) { 15 | history.push('/spot/create') 16 | } else { 17 | setShowSignUp(true) 18 | } 19 | }; 20 | 21 | return ( 22 | 29 | 40 | 99 | 100 | ); 101 | }; 102 | 103 | export default NavBar; 104 | -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | 5 | import BookingPageComponent from "../../BookingPageComponent/index"; 6 | import FundingComponent from "../../FundingComponent/index" 7 | import "./SpotPage.css" 8 | import { getSpot } from "../../../store/spot"; 9 | 10 | 11 | const SpotPage = () => { 12 | const dispatch = useDispatch() 13 | const spotId = Number.parseInt(useParams().spotId) 14 | const spotSelector = useSelector(state => state.spots) 15 | const spotAvailability = useSelector(state => state.spots.availability) 16 | // const spotTitle = spotSelector.spot.title 17 | 18 | const [spotState, setSpotState] = useState("") 19 | 20 | useEffect(() => { 21 | setSpotState(dispatch(getSpot(spotId))) 22 | }, [dispatch]) 23 | 24 | 25 | return ( 26 | <> 27 |
    28 | {spotSelector.spot && ( 29 |
    30 |
    31 |
    32 | Spot Image 33 |
    34 |
    35 |
    36 |

    {spotSelector.spot.title}

    37 |
    38 |

    {spotSelector.spot.city}, {spotSelector.spot.state}

    39 |
    40 |

    {spotSelector.spot.description}

    41 |
    42 |

    Max Capacity: {spotSelector.spot.capacity}

    43 |

    Current Availability: {spotSelector.spot.availability}

    44 |
    45 |
    46 |
    47 | 48 |
    49 |
    50 | 51 |
    52 |
    53 |
    54 |
    55 |
    56 | 57 | // Spot Image 58 | //
    {spotSelector.spot.title}
    59 | //
    {spotSelector.spot.city}, {spotSelector.spot.state}
    60 | //
    {spotSelector.spot.description}
    61 | //
    Max Capacity: {spotSelector.spot.capacity}
    62 | //
    Current Availability: {spotSelector.spot.availability}
    63 | //
    64 | // 65 | // 66 | //
    67 | 68 | )} 69 |
    70 | 71 | ) 72 | 73 | } 74 | 75 | export default SpotPage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CareBnB 2 | 3 | --- 4 | 5 | **CareBnB** is a clone of **[AirBnB](https://www.airbnb.com/)** but instead of serving guests who are traveling, CareBnB serves those in need of shelter. The project was inspired by the 2021 events in Northern Texas, where many Texans were left without power and water. 6 | 7 |

    Try the site live: Here | Check out our documentation

    8 | 9 | ## How to run the site locally 10 | 11 | - Clone the repo 12 | - In the route directory, use the command ```pipenv install``` all dependencies 13 | - In the react-app directory, use the command ```npm install``` to install all dependencies 14 | - Make a copy of the .env.example file and edit to match local db configuration 15 | - Create a file in /CareBnB/react-app/src/components/GoogleMapsComponent called apikey.js with one export const named "googleApiKey" 16 | - Create a file in /CareBnB/react-app/src/aws called keys.js with one export const named "SECRET_ACCESS_KEY" 17 | - Create the database and user in psql 18 | * In a pipenv shell, run all migrations with ```flask db upgrade``` 19 | * Seed all data with ```flask db seed all``` 20 | - In the route directory, use the command ```flask run``` to run the backend server 21 | - In the react-app directory, use the comman ```npm start``` to run the front end 22 | 23 | ## Technologies used in CareBnB 24 | 25 | **JavaScript** 26 | 27 | **Python** 28 | 29 | **SQLAlchemy** 30 | 31 | **Flask** 32 | 33 | **React** 34 | 35 | **Redux** 36 | 37 | **React Google Maps** 38 | 39 | **Amazon Web Services** 40 | 41 | **React Bootstrap** 42 | 43 | **Heroku** 44 | 45 | ## Features that we implemented 46 | 47 | 48 | * Users can **log in** or **sign up** to access some functionality of the site. 49 | * A logged in user has the ability to **host a spot** that displays how many guests they can accept. 50 | * A logged in user can **book a spot**, and selecte how many guests they are booking for. 51 | * The **search** bar can locate using a search term and specify how many available spots. 52 | * A logged in user can utilize **google maps** to identify a spot close to them. 53 | 54 | ## Challenges throughout the development process 55 | We faced a few challenges while we were building CareBnB: 56 | 57 | 1. Implementing google maps to allow users to search spots by their coordinates 58 | 59 | 2. Implementing AWS to allow users to upload an image as a file instead of a image url 60 | 61 | ## Developers 62 | 63 | Developer 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
    Mustafa MousaLane JohnsQuintin HullJung Park
    @mustafaomousa@lanejohns@QuintinHull@Jummies
    84 | 85 | -------------------------------------------------------------------------------- /react-app/src/components/HomePageComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { Carousel } from "react-bootstrap"; 4 | import { getTopAvailableSpots } from "../../store/spot"; 5 | import { googleApiKey } from "../GoogleMapsComponent/apikey"; 6 | import SearchBar from "../SearchComponent/SearchBar"; 7 | import SpotViewMini from "../SpotComponent/SpotViewMini"; 8 | import { WrappedGoogleMap } from "../GoogleMapsComponent/index"; 9 | import LoginModal from "../auth/LoginForm"; 10 | import SignUpModal from "../auth/SignUpForm"; 11 | import FooterComponent from "../FooterComponent" 12 | 13 | import "./home-page.css"; 14 | import NewUser from "./newUser"; 15 | 16 | const HomePageComponent = (props) => { 17 | const dispatch = useDispatch(); 18 | const { authenticated, setShowLogin, setShowSignUp } = props 19 | const [show, setShow] = useState(true); 20 | const available_spots = useSelector((state) => state.spots.available_spots); 21 | 22 | useEffect(() => { 23 | dispatch(getTopAvailableSpots()); 24 | }, [dispatch]); 25 | 26 | return ( 27 |
    28 |
    29 | 34 |
    35 |
    36 | 37 |
    38 | 39 |
    40 | {authenticated === false && } 41 |
    42 |

    Experience a spot

    43 |
    44 | {available_spots && 45 | available_spots.map((spot, idx) => ( 46 | 47 | ))} 48 |
    49 |
    50 |
    51 |
    52 |

    53 | "We aim to ensure no human being has to go to sleep without 54 | shelter...." 55 |

    56 |
    57 |
    58 | 59 | 60 |

    230

    61 | 62 |

    People currently sheltered

    63 |
    64 |
    65 | 66 |

    Thank you username for your donation

    67 | 72 | 73 |

    Donated 2 minutes ago

    74 |
    75 |
    76 |
    77 |
    78 |
    79 |
    82 |
    83 | } 86 | containerElement={
    } 87 | mapElement={
    } 88 | /> 89 | 90 |
    91 |
    92 | ); 93 | }; 94 | 95 | export default HomePageComponent; 96 | -------------------------------------------------------------------------------- /react-app/src/store/spot.js: -------------------------------------------------------------------------------- 1 | const LOAD = "spot/getAllSpots"; 2 | const LOAD_ONE = "spot/getOneSpot"; 3 | const LOAD_AVAILABLE = "spot/getAvailableSpots"; 4 | const LOAD_SEARCH = "spot/getAllSearchedSpots"; 5 | const CREATE_SPOT = "spot/createNewSpot"; 6 | const DELETE_SPOT = "spot/deleteASpot"; 7 | 8 | const getAllSearchedSpots = (spots) => { 9 | return { 10 | type: LOAD_SEARCH, 11 | payload: spots, 12 | }; 13 | }; 14 | 15 | const getAllSpots = (spots) => { 16 | return { 17 | type: LOAD, 18 | payload: spots, 19 | }; 20 | }; 21 | 22 | const getAvailableSpots = (spots) => { 23 | return { 24 | type: LOAD_AVAILABLE, 25 | payload: spots, 26 | }; 27 | }; 28 | 29 | const createNewSpot = (spot) => { 30 | return { 31 | type: CREATE_SPOT, 32 | payload: spot, 33 | }; 34 | }; 35 | 36 | const deleteASpot = (spot) => { 37 | return { 38 | type: DELETE_SPOT, 39 | payload: spot, 40 | }; 41 | }; 42 | 43 | const getOneSpot = (spot) => { 44 | return { 45 | type: LOAD_ONE, 46 | payload: spot, 47 | }; 48 | }; 49 | 50 | export const getSpots = () => async (dispatch) => { 51 | const response = await fetch("/api/spots/"); 52 | const spots = await response.json(); 53 | return dispatch(getAllSpots(spots)); 54 | }; 55 | 56 | export const searchSpots = (guest_size, city) => async (dispatch) => { 57 | const response = await fetch(`/api/spots/search/${guest_size}&${city}`); 58 | const spots = await response.json(); 59 | return dispatch(getAllSearchedSpots(spots)); 60 | }; 61 | 62 | export const getSpot = (id) => async (dispatch) => { 63 | const response = await fetch(`/api/spots/${id}`); 64 | const spot = await response.json(); 65 | dispatch(getOneSpot(spot)); 66 | return spot; 67 | }; 68 | 69 | export const createSpot = ({ 70 | image_url, 71 | title, 72 | address, 73 | city, 74 | state, 75 | zipcode, 76 | description, 77 | capacity, 78 | availability, 79 | latitude, 80 | longitude, 81 | }) => async (dispatch) => { 82 | const response = await fetch("/api/spots/", { 83 | method: "POST", 84 | headers: { 85 | "Content-Type": "application/json", 86 | }, 87 | body: JSON.stringify({ 88 | image_url, 89 | title, 90 | address, 91 | city, 92 | state, 93 | zipcode, 94 | description, 95 | capacity, 96 | availability, 97 | latitude, 98 | longitude, 99 | }), 100 | }); 101 | const spot = await response.json(); 102 | dispatch(createNewSpot(spot)); 103 | return spot; 104 | }; 105 | 106 | export const deleteSpot = ({ spotId }) => async (dispatch) => { 107 | const response = await fetch(`/api/spots/${spotId}`, { 108 | method: "DELETE", 109 | headers: { 110 | "Content-Type": "application/json", 111 | }, 112 | }); 113 | const spot = await response.json(); 114 | return dispatch(deleteASpot(spot)); 115 | }; 116 | 117 | export const getTopAvailableSpots = () => async (dispatch) => { 118 | const response = await fetch("/api/spots/top-available"); 119 | const spots = await response.json(); 120 | return dispatch(getAvailableSpots(spots)); 121 | }; 122 | 123 | const initialState = {}; 124 | 125 | const spotReducer = (state = initialState, action) => { 126 | let newState; 127 | switch (action.type) { 128 | case LOAD: 129 | newState = Object.assign({}, state, { ...action.payload }); 130 | return newState; 131 | case LOAD_ONE: 132 | newState = Object.assign({}, state, { ...action.payload }); 133 | return newState; 134 | case LOAD_AVAILABLE: 135 | newState = Object.assign({}, state, { ...action.payload }); 136 | return newState; 137 | case LOAD_SEARCH: 138 | newState = Object.assign({}, state, { ...action.payload }); 139 | return newState; 140 | case CREATE_SPOT: 141 | const new_spot = action.payload.spot; 142 | const all_spots = state.all_spots; 143 | newState = { all_spots: { ...all_spots, ...new_spot } }; 144 | return newState; 145 | case DELETE_SPOT: 146 | const deleted_spot = action.payload.spot; 147 | newState = Object.assign({}, state); 148 | delete newState.all_spots[deleted_spot.id]; 149 | return newState; 150 | default: 151 | return state; 152 | } 153 | }; 154 | 155 | export default spotReducer; 156 | -------------------------------------------------------------------------------- /react-app/src/components/SearchComponent/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useLocation } from 'react-router-dom'; 4 | 5 | import InputGroup from 'react-bootstrap/InputGroup' 6 | import FormControl from 'react-bootstrap/FormControl' 7 | import Button from 'react-bootstrap/Button'; 8 | import { ArrowRightCircleFill } from 'react-bootstrap-icons'; 9 | import { Redirect, useHistory } from 'react-router-dom'; 10 | import { getSpots, searchSpots } from '../../../store/spot'; 11 | 12 | import './search-bar.css' 13 | 14 | const SearchBar = () => { 15 | const curr_location = useLocation() 16 | const dispatch = useDispatch() 17 | const history = useHistory() 18 | const [location, setLocation] = useState(''); 19 | const [guestCount, setGuestCount] = useState(1) 20 | 21 | const spots = useSelector(state => state.spots.all_spots) 22 | 23 | const updateLocation = (event) => setLocation(event.target.value); 24 | const updateGuestCount = (event) => setGuestCount(event.target.value); 25 | 26 | 27 | useEffect(() => { 28 | dispatch(getSpots()) 29 | }, [dispatch]) 30 | 31 | const locationResults = (spots, location) => { 32 | for (let spotId in spots) { 33 | if (spots[spotId].city.toLowerCase().includes(location.toLowerCase())) { 34 | return (

    setLocation(e.target.id)} id={`${spots[spotId].city}`}>{spots[spotId].city}

    ) 35 | } 36 | } 37 | } 38 | 39 | const search_bar = document.getElementById('search-bar-container-scrolled') 40 | const search_button = document.getElementById('search-butt') 41 | 42 | document.onscroll = () => { 43 | let top = window.pageYOffset 44 | if (top >= 75 && search_bar) { 45 | search_button.onmouseover = () => { search_button.style.color = '#c0c5c3' } 46 | search_button.onmouseout = () => { search_button.style.color = '#DAE2DF' } 47 | search_button.style.transition = "all .2s ease" 48 | search_bar.style.transition = "all .2s ease" 49 | 50 | } else if (top < 75 && search_bar) { 51 | search_button.onmouseover = () => { search_button.style.color = '#616060' } 52 | search_button.onmouseout = () => { search_button.style.color = '#6D696A' } 53 | } 54 | } 55 | 56 | 57 | 58 | const executeSearch = (e) => { 59 | dispatch(searchSpots(guestCount, location)) 60 | return history.push(`/locate?city=${location}&guest=${guestCount}`) 61 | } 62 | 63 | useEffect(() => { 64 | document.getElementById("test-input").addEventListener("keyup", (e) => { 65 | e.preventDefault() 66 | if (e.keyCode === 13) { 67 | return executeSearch(e) 68 | } 69 | }) 70 | document.getElementById("test-input2").addEventListener("keyup", (e) => { 71 | e.preventDefault() 72 | if (e.keyCode === 13) { 73 | return executeSearch(e) 74 | } 75 | }) 76 | }, [location, guestCount]) 77 | 78 | return ( 79 |
    80 |
    81 | 82 | 83 | 89 | 90 | 91 | 92 | 100 | 101 | 102 | {/*
    103 | {spots && location && locationResults(spots, location)} 104 |
    */} 105 | 106 |
    107 | ) 108 | } 109 | 110 | export default SearchBar -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 3 | import { Provider as ReduxProvider } from "react-redux"; 4 | import LoginModal from "./components/auth/LoginForm"; 5 | import SignUpModal from "./components/auth/SignUpForm"; 6 | import NavBar from "./components/NavBar"; 7 | import ProtectedRoute from "./components/auth/ProtectedRoute"; 8 | import UsersList from "./components/UsersList"; 9 | import User from "./components/User"; 10 | import { authenticate } from "./services/auth"; 11 | import configureStore from "./store"; 12 | 13 | import HomePage from "./components/HomePageComponent"; 14 | import SpotPage from "./components/SpotComponent/SpotPage"; 15 | import BookingPageComponent from "./components/BookingPageComponent"; 16 | import FundingComponent from "./components/FundingComponent"; 17 | import SearchPage from "./components/SearchComponent/SearchPage"; 18 | import SpotCreate from "./components/SpotComponent/SpotCreate"; 19 | import FooterComponent from "./components/FooterComponent" 20 | 21 | const store = configureStore(); 22 | 23 | function App() { 24 | const [authenticated, setAuthenticated] = useState(false); 25 | const [loaded, setLoaded] = useState(false); 26 | const [showLogin, setShowLogin] = useState(false) 27 | const [showSignUp, setShowSignUp] = useState(false) 28 | 29 | useEffect(() => { 30 | (async () => { 31 | const user = await authenticate(); 32 | if (!user.errors) { 33 | setAuthenticated(true); 34 | } 35 | setLoaded(true); 36 | })(); 37 | }, []); 38 | 39 | if (!loaded) { 40 | return null; 41 | } 42 | 43 | return ( 44 | 45 | 46 | 47 | setShowLogin(false)} authenticated={authenticated} setAuthenticated={setAuthenticated} setShowLogin={setShowLogin} setShowSignUp={setShowSignUp} /> 48 | setShowSignUp(false)} authenticated={authenticated} setAuthenticated={setAuthenticated} setShowLogin={setShowLogin} setShowSignUp={setShowSignUp} /> 49 | 50 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | {/* Home Page: */} 68 | 69 | 70 | 71 | 72 | {/* testing spot create */} 73 | 79 | 80 | 81 | 82 | {/* Spot Page */} 83 | 89 | 90 | 91 | 92 | {/* Booking Page */} 93 | 99 | 100 | 101 | 102 | {/* Funding Page */} 103 | 109 | 110 | 111 | 112 | {/* Search Page */} 113 | 118 | 119 | 120 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | ); 132 | } 133 | 134 | export default App; 135 | -------------------------------------------------------------------------------- /react-app/src/components/auth/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Redirect } from 'react-router-dom'; 3 | import { signUp } from '../../services/auth'; 4 | import { Modal, Button } from "react-bootstrap"; 5 | 6 | // const SignUpForm = ({ authenticated, setAuthenticated }) => { 7 | // const [firstName, setFirstName] = useState(""); 8 | // const [lastName, setLastName] = useState(""); 9 | // const [email, setEmail] = useState(""); 10 | // const [password, setPassword] = useState(""); 11 | // const [repeatPassword, setRepeatPassword] = useState(""); 12 | 13 | // const onSignUp = async (e) => { 14 | // e.preventDefault(); 15 | // if (password === repeatPassword) { 16 | // const user = await signUp(firstName, lastName, email, password); 17 | // if (!user.errors) { 18 | // setAuthenticated(true); 19 | // } 20 | // } 21 | // }; 22 | 23 | // const updateFirstName = (e) => { 24 | // setFirstName(e.target.value); 25 | // }; 26 | 27 | // const updateLastName = (e) => { 28 | // setLastName(e.target.value); 29 | // }; 30 | 31 | // const updateEmail = (e) => { 32 | // setEmail(e.target.value); 33 | // }; 34 | 35 | // const updatePassword = (e) => { 36 | // setPassword(e.target.value); 37 | // }; 38 | 39 | // const updateRepeatPassword = (e) => { 40 | // setRepeatPassword(e.target.value); 41 | // }; 42 | 43 | // if (authenticated) { 44 | // return ; 45 | // } 46 | 47 | // return ( 48 | //
    49 | //
    50 | // 51 | // 57 | //
    58 | //
    59 | // 60 | // 66 | //
    67 | //
    68 | // 69 | // 75 | //
    76 | //
    77 | // 78 | // 84 | //
    85 | //
    86 | // 87 | // 94 | //
    95 | // 96 | //
    97 | // ); 98 | // }; 99 | 100 | // export default SignUpForm; 101 | 102 | const SignUpModal = (props) => { 103 | const [firstName, setFirstName] = useState(""); 104 | const [lastName, setLastName] = useState(""); 105 | const [email, setEmail] = useState(""); 106 | const [password, setPassword] = useState(""); 107 | const [repeatPassword, setRepeatPassword] = useState(""); 108 | 109 | const { authenticated, setAuthenticated } = props 110 | 111 | const switchToLogin = () => { 112 | props.setShowSignUp(false); 113 | props.setShowLogin(true); 114 | }; 115 | 116 | const onSignUp = async (e) => { 117 | e.preventDefault(); 118 | if (password === repeatPassword) { 119 | const user = await signUp(firstName, lastName, email, password); 120 | if (!user.errors) { 121 | setAuthenticated(true); 122 | } 123 | } 124 | }; 125 | 126 | const updateFirstName = (e) => { 127 | setFirstName(e.target.value); 128 | }; 129 | 130 | const updateLastName = (e) => { 131 | setLastName(e.target.value); 132 | }; 133 | 134 | const updateEmail = (e) => { 135 | setEmail(e.target.value); 136 | }; 137 | 138 | const updatePassword = (e) => { 139 | setPassword(e.target.value); 140 | }; 141 | 142 | const updateRepeatPassword = (e) => { 143 | setRepeatPassword(e.target.value); 144 | }; 145 | 146 | if (authenticated) { 147 | return ; 148 | } 149 | 150 | 151 | return ( 152 | 158 | 159 | 160 | Sign Up 161 | 162 | 163 | 164 |
    165 |
    166 | 167 | 173 |
    174 |
    175 | 176 | 182 |
    183 |
    184 | 185 | 191 |
    192 |
    193 | 194 | 200 |
    201 |
    202 | 203 | 210 |
    211 |
    212 | 213 | 214 |
    215 | 216 |
    217 |
    218 | 219 | 220 | 221 |
    222 | ); 223 | } 224 | 225 | export default SignUpModal -------------------------------------------------------------------------------- /react-app/src/components/SpotComponent/SpotCreate/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import FormControl from "react-bootstrap/FormControl"; 4 | import Button from "react-bootstrap/Button"; 5 | import FormGroup from "react-bootstrap/FormGroup"; 6 | import FormLabel from "react-bootstrap/FormLabel"; 7 | import Form from "react-bootstrap/Form"; 8 | import Row from "react-bootstrap/Row"; 9 | import Col from "react-bootstrap/Col"; 10 | import Container from "react-bootstrap/Container"; 11 | import UploadPictureS3Client from "../../../aws/s3"; 12 | import { createSpot, getTopAvailableSpots } from "../../../store/spot"; 13 | import { useHistory } from "react-router-dom"; 14 | import Geocode from "react-geocode"; 15 | import { googleApiKey } from "../../GoogleMapsComponent/apikey"; 16 | 17 | import "./SpotCreate.css"; 18 | 19 | const SpotCreate = () => { 20 | const dispatch = useDispatch(); 21 | const history = useHistory(); 22 | const createdSpot = useSelector((state) => state.spots.all_spots); 23 | 24 | const states = [ 25 | "AL", 26 | "AK", 27 | "AZ", 28 | "AR", 29 | "CA", 30 | "CO", 31 | "CT", 32 | "DE", 33 | "FL", 34 | "GA", 35 | "HI", 36 | "ID", 37 | "IL", 38 | "IN", 39 | "IA", 40 | "KS", 41 | "KY", 42 | "LA", 43 | "ME", 44 | "MD", 45 | "MA", 46 | "MI", 47 | "MN", 48 | "MS", 49 | "MO", 50 | "MT", 51 | "NE", 52 | "NV", 53 | "NH", 54 | "NJ", 55 | "NM", 56 | "NY", 57 | "NC", 58 | "ND", 59 | "OH", 60 | "OK", 61 | "OR", 62 | "PA", 63 | "RI", 64 | "SC", 65 | "SD", 66 | "TN", 67 | "TX", 68 | "UT", 69 | "VT", 70 | "VA", 71 | "WA", 72 | "WV", 73 | "WI", 74 | "WY", 75 | ]; 76 | 77 | const [imageUrl, setImageUrl] = useState(""); 78 | const [title, setTitle] = useState(""); 79 | const [address, setAddress] = useState(""); 80 | const [city, setCity] = useState(""); 81 | const [state, setState] = useState(states[0]); 82 | const [zipcode, setZipcode] = useState(""); 83 | const [description, setDescription] = useState(""); 84 | const [capacity, setCapacity] = useState(""); 85 | 86 | Geocode.setApiKey(googleApiKey); 87 | Geocode.setLanguage("en"); 88 | Geocode.setLocationType("ROOFTOP"); 89 | 90 | const getLat = (address, city, state, zipcode) => { 91 | return Geocode.fromAddress(`${address} ${city}, ${state} ${zipcode}`).then( 92 | (response) => { 93 | const { lat } = response.results[0].geometry.location; 94 | return lat; 95 | }, 96 | (error) => { 97 | console.error(error); 98 | } 99 | ); 100 | }; 101 | 102 | const getLng = (address, city, state, zipcode) => { 103 | return Geocode.fromAddress(`${address} ${city}, ${state} ${zipcode}`).then( 104 | (response) => { 105 | const { lng } = response.results[0].geometry.location; 106 | return lng; 107 | }, 108 | (error) => { 109 | console.error(error); 110 | } 111 | ); 112 | }; 113 | 114 | const handleSubmit = async (event) => { 115 | event.preventDefault(); 116 | const lat = await getLat(address, city, state, zipcode); 117 | const lng = await getLng(address, city, state, zipcode); 118 | const newSpot = { 119 | image_url: imageUrl, 120 | title, 121 | address, 122 | city, 123 | state, 124 | zipcode, 125 | description, 126 | capacity, 127 | latitude: lat, 128 | longitude: lng, 129 | }; 130 | 131 | let addedSpot = await dispatch(createSpot(newSpot)); 132 | 133 | history.push(`/spot/${addedSpot.spot.id}`); 134 | }; 135 | 136 | const uploadFile = (e) => { 137 | e.preventDefault(); 138 | 139 | UploadPictureS3Client.uploadFile( 140 | e.target.files[0], 141 | `spot-${title}-${new Date()}` 142 | ).then((data) => setImageUrl(data.location)); 143 | }; 144 | 145 | return ( 146 |
    147 | 148 |
    149 | 150 | 151 |
    Register Your Spot
    152 | 153 |
    154 | 155 | 156 | 157 | 158 | Image URL:{" "} 159 | 160 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | Title:{" "} 175 | 176 | setTitle(e.target.value)} 183 | > 184 | 185 | 186 | 187 |
    188 | 189 | 190 | 191 | 192 | Address:{" "} 193 | 194 | setAddress(e.target.value)} 201 | > 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | City:{" "} 210 | 211 | setCity(e.target.value)} 218 | > 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | State:{" "} 227 | 228 | setState(e.target.value)} 233 | > 234 | {states.map((state) => ( 235 | 236 | ))} 237 | 238 | 239 | 240 | 241 | 242 | 243 | Zipcode:{" "} 244 | 245 | setZipcode(e.target.value)} 251 | > 252 | 253 | 254 | 255 |
    256 | 257 | 258 | 259 | 260 | Description:{" "} 261 | 262 | setDescription(e.target.value)} 269 | > 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | Capacity:{" "} 278 | 279 | setCapacity(e.target.value)} 286 | > 287 | 288 | 289 | 290 | 291 | 292 | 295 | 296 | 297 |
    298 |
    299 |
    300 | ); 301 | }; 302 | 303 | export default SpotCreate; 304 | -------------------------------------------------------------------------------- /app/seeds/spots.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Spot 2 | 3 | 4 | def seed_spots(): 5 | 6 | spot_1 = Spot(image_url='https://m.media-amazon.com/images/I/61cKLDstV3L._AC_SX522_.jpg', title="Gen's Orphanage", address='7567 Court Ave.', 7 | city='Oviedo', state='FL', zipcode=32765, description='Amazing 10 bedroom home.', capacity=20, availability=20, latitude=28.6700, longitude=81.2081, host_id=2) 8 | 9 | spot_2 = Spot(image_url='https://www.bostondesignguide.com/sites/default/files/architecturaldesign-summerhomes-1.jpg', title="Juliet's Summer Home", address='762 Old Summerhouse St.', 10 | city='Goodlettsville', state='TN', zipcode=37072, description='Beautiful summer home located in the Tennesse hills', capacity=4, availability=4, latitude=36.3231, longitude=-86.7133, host_id=3) 11 | 12 | spot_3 = Spot(image_url='https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/bojnice-castle-1603142898.jpg', title="Alfredo's Castle", address='84 Courtland St.', 13 | city='Fremont', state='OH', zipcode=43420, description='Stunning countryside castle ', capacity=500, availability=500, latitude=41.3503, longitude=-83.1219, host_id=4) 14 | 15 | spot_4 = Spot(image_url='https://images.mansionglobal.com/im-191825?width=1280?width=1280&size=1', title="Ed's Mansion", address='11 Pleasant Avenue', 16 | city='Fenton', state='MI', zipcode=48430, description='Amazing 10 bedroom home.', capacity=50, availability=50, latitude=42.7978, longitude=-83.7049, host_id=5) 17 | 18 | spot_5 = Spot(image_url='https://gambrick.com/wp-content/uploads/2019/05/small-house-colors-brick.jpg', title="Ranch style", address='7563 Fith Street', 19 | city='Fort Worth', state='TX', zipcode=76006, description='Ranch style home off busy street in Fort Worth', capacity=5, availability=5, latitude=32.7555, longitude=-97.3308, host_id=5) 20 | 21 | spot_6 = Spot(image_url='https://www.rocketmortgage.com/resources-cmsassets/RocketMortgage.com/Article_Images/Large_Images/TypesOfHomes/types-of-homes-hero.jpg', title="Cozy family home", address='5 Wellington St', 22 | city='Lubbock', state='TX', zipcode=79382, description='We are happy to host anyone in need of shelter.', capacity=1, availability=1, latitude=101.8313, longitude=-101.8552, host_id=6) 23 | 24 | spot_7 = Spot(image_url='https://static01.nyt.com/images/2019/06/25/realestate/25domestic-zeff/a1c1a1a36c9e4ff8adcb958c4276f28d-jumbo.jpg', title="All welcome here!", address='53 Mayfield Street', 25 | city='Amarillo', state='TX', zipcode=79101, description='Our home is open to anyone in need of shelter and a warm plate', capacity=3, availability=3, latitude=35.2220, longitude=-101.8313, host_id=7) 26 | 27 | spot_8 = Spot(image_url='https://static01.nyt.com/images/2019/06/25/realestate/25domestic-zeff/a1c1a1a36c9e4ff8adcb958c4276f28d-jumbo.jpg', title="Open to those in need!", address='368 Roosevelt Street', 28 | city='Oklahoma City', state='OK', zipcode=73008, description='We have room for five but can take more if willing to use sleeping bags', capacity=5, availability=5, latitude=35.4676, longitude=-97.5164, host_id=8) 29 | 30 | spot_9 = Spot(image_url='https://assets.themortgagereports.com/wp-content/uploads/2017/12/How-to-Buy-a-House-with-Low-Income-This-Year.jpg', title="A place to stay dry and warm!", address='9371 Foxrun Road', 31 | city='Midland', state='TX', zipcode=79701, description='When the weather is bad, we open our home for 10 people in need.', capacity=10, availability=10, latitude=31.9973, longitude=-102.0779, host_id=9) 32 | 33 | spot_10 = Spot(image_url='https://ei.marketwatch.com/Multimedia/2019/05/22/Photos/ZQ/MW-HK104_lillev_20190522122922_ZQ.jpg', title="Tudor style home", address='8351 S. Holly Avenue', 34 | city='Abilene', state='TX', zipcode=79601, description='', capacity=1, availability=1, latitude=32.4487, longitude=-99.7331, host_id=5) 35 | 36 | spot_11 = Spot(image_url='https://www.rocketmortgage.com/resources-cmsassets/RocketMortgage.com/Article_Images/Large_Images/Stock-Suburban-Home.jpg', title="Bunk-beds galore", address='8013C Boston Ave', 37 | city='San Angelo', state='TX', zipcode=76901, description='We have a room with two sets of bunk-beds that is great for kids', capacity=7, availability=7, latitude=31.4638, longitude=-100.4370, host_id=6) 38 | 39 | spot_12 = Spot(image_url='https://image.cnbcfm.com/api/v1/image/104559560-Jackson_Mississippi.jpg', title="Basement level, fully functional", address='9616 E. Kingston Dr', 40 | city='Wichita Falls', state='TX', zipcode=76301, description='Our home has a basement that is fully functional with a kitchen, bathroom, and two bedrooms.', capacity=8, availability=8, latitude=33.9137, longitude=-98.4934, host_id=7) 41 | 42 | spot_13 = Spot(image_url='https://charlotteaxios-charlotteagenda.netdna-ssl.com/wp-content/uploads/2019/07/July-open-houses-header-1.jpg', title="Safe Shelter", address='513 North Cambridge Ave', 43 | city='Waco', state='TX', zipcode=76633, description='Stay as long as you need!', capacity=5, availability=3, latitude=31.5493, longitude=-97.1467, host_id=8) 44 | 45 | spot_14 = Spot(image_url='https://media-cdn.wehco.com/img/photos/2019/09/06/190905foresthillhistorichomestour05b8900336972.jpg', title="One night only", address='63 Armstrong Court', 46 | city='Tyler', state='TX', zipcode=75701, description='We can host up to 2 guests for a max of one night at a time.', capacity=2, availability=2, latitude=32.3513, longitude=-95.3011, host_id=9) 47 | 48 | spot_15 = Spot(image_url='https://empire-s3-production.bobvila.com/slides/8507/original/putty_house.jpg', title="Safe neighborhood", address='23 Bay Meadows Dr', 49 | city='Paris', state='TX', zipcode=75460, description='Your average home in a safe neighborhood. Please dont hesitate to book with us!', capacity=3, availability=3, latitude=33.6609, longitude=-95.5555, host_id=5) 50 | 51 | spot_16 = Spot(image_url='https://cdn.houseplansservices.com/product/jbo1rrf0j7s4ebo15acfa89ar/w800x533.jpg', title="Room for your dogs!", address='79 S. Cedar Street', 52 | city='Guthrie', state='TX', zipcode=79236, description='We have a fenced in backyard so please bring your furry friends too!', capacity=3, availability=3, latitude=33.6207, longitude=-100.3228, host_id=6) 53 | 54 | spot_17 = Spot(image_url='https://photos.zillowstatic.com/fp/ef2b1d11bc5c7b1a33c59bebd4bf338b-p_e.jpg', title="Farm House", address='67 Shore Road', 55 | city='Fresno', state='CA', zipcode=93650, description='We have a farm house with a private guest house. Perfect for an entire family in need.', capacity=7, availability=5, latitude=36.7378, longitude=-119.7871, host_id=7) 56 | 57 | spot_18 = Spot(image_url='https://www.theday.com/storyimage/NL/20201105/NWS01/201109888/AR/0/AR-201109888.jpg', title="Garage Apartment", address='370 Arrowhead Street', 58 | city='Portland', state='OR', zipcode=97035, description='Stay dry in our apartment attached to our garage. You will have plenty of privacy', capacity=10, availability=10, latitude=45.5051, longitude=-122.6750, host_id=8) 59 | 60 | spot_19 = Spot(image_url='https://images.adsttc.com/media/images/5c2e/ec1d/08a5/e549/4e00/0045/large_jpg/Unjung-dong_brick_house_(1).jpg', title="Room for one", address='55 Evergreen Rd', 61 | city='Philadelphia', state='PA', zipcode=19019, description='We have a spare bedroom that can easily accommodate someone looking for shelter', capacity=1, availability=1, latitude=39.9526, longitude=-75.1652, host_id=9) 62 | 63 | spot_20 = Spot(image_url='https://media-cdn.wehco.com/img/photos/2014/09/21/LM-Monaco-home-13_t1070_ha8041694561a87fcc20f356e91cfcd09434b5e1b.jpg', title="Warm house, clean water, & internet", address='108 Rosewood Street', 64 | city='Jefferson City', state='MO', zipcode=65101, description='Whatever the reason, please dont hesitate to book a stay with us.', capacity=2, availability=1, latitude=38.5767, longitude=-92.1735, host_id=5) 65 | 66 | spot_21 = Spot(image_url='https://historicindianapolis.com/wp-content/uploads/2012/09/1531-Broadway-01.jpg', title="Well heated family home", address='91 Queen St', 67 | city='Syracuse', state='NY', zipcode=13201, description='When the neighborhood loses electricity you can count on us for help.', capacity=4, availability=2, latitude=43.0481, longitude=-76.1474, host_id=6) 68 | 69 | spot_22 = Spot(image_url='https://i.pinimg.com/originals/ba/b0/f8/bab0f85e67ab72a2d95995cf434ef506.jpg', title="Family home on river bluff", address='7 South Clinton St', 70 | city='Little Rock', state='AR', zipcode=72002, description='We would be more than happy to host anyone in need of food or a place to sleep.', capacity=8, availability=8, latitude=34.746, longitude=-92.2896, host_id=7) 71 | 72 | spot_23 = Spot(image_url='https://charlotteaxios-charlotteagenda.netdna-ssl.com/wp-content/uploads/2019/07/July-open-houses-header-1.jpg', title="Stay in the Shade", address='578 Colonial Ave', 73 | city='Tucson', state='AZ', zipcode=85641, description='When you are find yourself in an emergency, let us help and book with us', capacity=5, availability=3, latitude=32.2226, longitude=-110.9747, host_id=8) 74 | 75 | spot_24 = Spot(image_url='https://www.rocketmortgage.com/resources-cmsassets/RocketMortgage.com/Article_Images/Large_Images/Stock-Suburban-Home.jpg', title="Blizzard House", address='8341 Aspen Road', 76 | city='Denver', state='CO', zipcode=80014, description='Our house never loses power in a blizzard. Book with us to stay warm!', capacity=3, availability=3, latitude=39.7392, longitude=-104.9903, host_id=9) 77 | 78 | spot_25 = Spot(image_url='https://static01.nyt.com/images/2019/06/25/realestate/25domestic-zeff/a1c1a1a36c9e4ff8adcb958c4276f28d-jumbo.jpg', title="A break from the cold", address='976 Garfield Lane', 79 | city='Boise', state='ID', zipcode=83701, description='It can get cold in Boise. If you ever need to warm up, book with us!', capacity=1, availability=1, latitude=43.6150, longitude=-116.2023, host_id=5) 80 | 81 | db.session.add(spot_1) 82 | db.session.add(spot_2) 83 | db.session.add(spot_3) 84 | db.session.add(spot_5) 85 | db.session.add(spot_6) 86 | db.session.add(spot_7) 87 | db.session.add(spot_8) 88 | db.session.add(spot_9) 89 | db.session.add(spot_10) 90 | db.session.add(spot_11) 91 | db.session.add(spot_12) 92 | db.session.add(spot_13) 93 | db.session.add(spot_14) 94 | db.session.add(spot_15) 95 | db.session.add(spot_16) 96 | db.session.add(spot_17) 97 | db.session.add(spot_18) 98 | db.session.add(spot_19) 99 | db.session.add(spot_20) 100 | db.session.add(spot_21) 101 | db.session.add(spot_22) 102 | db.session.add(spot_23) 103 | db.session.add(spot_24) 104 | db.session.add(spot_25) 105 | 106 | db.session.commit() 107 | 108 | # Uses a raw SQL query to TRUNCATE the users table. 109 | # SQLAlchemy doesn't have a built in function to do this 110 | # TRUNCATE Removes all the data from the table, and resets 111 | # the auto incrementing primary key 112 | 113 | 114 | def undo_spots(): 115 | Spot.query.delete() 116 | db.session.commit() 117 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5c8e25a8a80e32d2d92baa84dd45978ac903380fdb4af3e0c53573583bbbc52d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 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:094b1d1c60d790649989d38d34f69e1ef07792366277a2cf88684d03495d018f", 46 | "sha256:1a13bd6050d1db4475f13e444e169b6fe872434922d38968c67cea9568cce2f0" 47 | ], 48 | "index": "pypi", 49 | "version": "==1.1.2" 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:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", 117 | "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" 118 | ], 119 | "markers": "python_version >= '3.4'", 120 | "version": "==3.1" 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:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", 306 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" 307 | ], 308 | "markers": "python_version >= '3.5'", 309 | "version": "==2.4.2" 310 | }, 311 | "autopep8": { 312 | "hashes": [ 313 | "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea", 314 | "sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443" 315 | ], 316 | "index": "pypi", 317 | "version": "==1.5.5" 318 | }, 319 | "isort": { 320 | "hashes": [ 321 | "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", 322 | "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" 323 | ], 324 | "markers": "python_version >= '3.6' and python_version < '4.0'", 325 | "version": "==5.7.0" 326 | }, 327 | "lazy-object-proxy": { 328 | "hashes": [ 329 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 330 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 331 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 332 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 333 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 334 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 335 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 336 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 337 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 338 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 339 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 340 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 341 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 342 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 343 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 344 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 345 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 346 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 347 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 348 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 349 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 350 | ], 351 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 352 | "version": "==1.4.3" 353 | }, 354 | "mccabe": { 355 | "hashes": [ 356 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 357 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 358 | ], 359 | "version": "==0.6.1" 360 | }, 361 | "psycopg2-binary": { 362 | "hashes": [ 363 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 364 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 365 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 366 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 367 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 368 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 369 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 370 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 371 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 372 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 373 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 374 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 375 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 376 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 377 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 378 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 379 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 380 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 381 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 382 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 383 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 384 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 385 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 386 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 387 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 388 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 389 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 390 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 391 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 392 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 393 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 394 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 395 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 396 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 397 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 398 | ], 399 | "index": "pypi", 400 | "version": "==2.8.6" 401 | }, 402 | "pycodestyle": { 403 | "hashes": [ 404 | "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", 405 | "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" 406 | ], 407 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 408 | "version": "==2.6.0" 409 | }, 410 | "pylint": { 411 | "hashes": [ 412 | "sha256:718b74786ea7ed07aa0c58bf572154d4679f960d26e9641cc1de204a30b87fc9", 413 | "sha256:e71c2e9614a4f06e36498f310027942b0f4f2fde20aebb01655b31edc63b9eaf" 414 | ], 415 | "index": "pypi", 416 | "version": "==2.6.2" 417 | }, 418 | "six": { 419 | "hashes": [ 420 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 421 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 422 | ], 423 | "index": "pypi", 424 | "version": "==1.15.0" 425 | }, 426 | "toml": { 427 | "hashes": [ 428 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 429 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 430 | ], 431 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 432 | "version": "==0.10.2" 433 | }, 434 | "wrapt": { 435 | "hashes": [ 436 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 437 | ], 438 | "version": "==1.12.1" 439 | } 440 | } 441 | } 442 | --------------------------------------------------------------------------------