├── .flaskenv ├── dev-requirements.txt ├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── env.py └── versions │ └── 3f40e31e33ee_.py ├── react-app ├── src │ ├── components │ │ ├── index.css │ │ ├── ReviewEditForm │ │ │ ├── reviewEditForm.css │ │ │ └── index.js │ │ ├── CreateReviewForm │ │ │ ├── createReviewForm.css │ │ │ └── index.js │ │ ├── auth │ │ │ ├── ProtectedRoute.js │ │ │ ├── LogoutButton.js │ │ │ ├── LoginForm.js │ │ │ └── SignUpForm.js │ │ ├── User.js │ │ ├── DeleteReview │ │ │ └── index.js │ │ ├── Splash │ │ │ └── index.js │ │ ├── SearchBar │ │ │ ├── SearchBar.css │ │ │ └── index.js │ │ ├── Booking │ │ │ └── index.js │ │ ├── One_booking │ │ │ └── index.js │ │ ├── NavBar.js │ │ ├── UsersList.js │ │ ├── BookSpotForm │ │ │ └── index.js │ │ ├── SearchResults │ │ │ └── index.js │ │ ├── Spot │ │ │ └── index.js │ │ ├── EditBookSpotForm │ │ │ └── index.js │ │ ├── SpotDetailsPage │ │ │ └── index.js │ │ └── SpotForm.js │ ├── index.css │ ├── index.js │ ├── store │ │ ├── index.js │ │ ├── location.js │ │ ├── image.js │ │ ├── session.js │ │ ├── booking.js │ │ ├── review.js │ │ └── spot.js │ ├── services │ │ └── auth.js │ └── App.js ├── .env.example ├── public │ ├── favicon.ico │ └── index.html ├── tailwind.config.js ├── .gitignore ├── README.md └── package.json ├── .gitignore ├── .DS_Store ├── app ├── models │ ├── db.py │ ├── __init__.py │ ├── location.py │ ├── image.py │ ├── booking.py │ ├── message.py │ ├── review.py │ ├── spot.py │ └── user.py ├── config.py ├── forms │ ├── __init__.py │ ├── spot_search_form.py │ ├── review_form.py │ ├── booking_form.py │ ├── spot_form.py │ ├── signup_form.py │ └── login_form.py ├── api │ ├── location_routes.py │ ├── user_routes.py │ ├── image_routes.py │ ├── spot_search_routes.py │ ├── booking_routes.py │ ├── review_routes.py │ ├── spot_routes.py │ └── auth_routes.py ├── seeds │ ├── __init__.py │ ├── images.py │ ├── bookings.py │ ├── reviews.py │ ├── users.py │ ├── locations.py │ └── spots.py └── __init__.py ├── .dockerignore ├── .vscode └── settings.json ├── .env.example ├── Dockerfile ├── requirements.txt ├── Pipfile ├── _work ├── movedProjectWork.txt └── heroku.txt ├── README.md └── Pipfile.lock /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.6 2 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /react-app/src/components/index.css: -------------------------------------------------------------------------------- 1 | @import "./output.css"; 2 | -------------------------------------------------------------------------------- /react-app/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:5000 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-kaseff/house_hopping/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/ethankaseff/.pyenv/versions/3.9.4/bin/python" 3 | } -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan-kaseff/house_hopping/HEAD/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | /* TODO Add site wide styles */ 2 | @tailwind base; 3 | 4 | @tailwind components; 5 | 6 | @tailwind utilities; 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=dosomethingsecret 4 | DATABASE_URL=postgresql://starter_redux_user:password@localhost/starter_redux 5 | -------------------------------------------------------------------------------- /react-app/src/components/ReviewEditForm/reviewEditForm.css: -------------------------------------------------------------------------------- 1 | textarea[type="text"] { 2 | border: 1px solid grey; 3 | } 4 | 5 | select { 6 | border: 1px solid grey; 7 | } 8 | -------------------------------------------------------------------------------- /react-app/src/components/CreateReviewForm/createReviewForm.css: -------------------------------------------------------------------------------- 1 | textarea[type="text"] { 2 | border: 1px solid grey; 3 | } 4 | 5 | select { 6 | border: 1px solid grey; 7 | } 8 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .location import Location 4 | from .spot import Spot 5 | from .booking import Booking 6 | from .review import Review 7 | from .image import Image 8 | from .message import Message 9 | -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm 3 | from .spot_form import SpotForm 4 | from .review_form import ReviewForm 5 | from .spot_search_form import SpotSearchForm 6 | from .booking_form import BookingForm 7 | 8 | -------------------------------------------------------------------------------- /react-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [require("tailwindcss"), require("autoprefixer")], 11 | }; 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/api/location_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import login_required, current_user 3 | from app.models import Location, db 4 | # from app.forms import LocationForm 5 | 6 | location_routes = Blueprint('locations', __name__) 7 | 8 | 9 | @location_routes.route('/') 10 | def get_locations(): 11 | locations = Location.query.all() 12 | return {"locations": [location.to_dict() for location in locations]} 13 | -------------------------------------------------------------------------------- /app/forms/spot_search_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField, TextField, DateField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class SpotSearchForm(FlaskForm): 7 | location = StringField('Location', validators=[DataRequired()]) 8 | start_date = DateField('Start Date', validators=[DataRequired()]) 9 | end_date = DateField('End Date', validators=[DataRequired()]) 10 | 11 | -------------------------------------------------------------------------------- /react-app/src/components/auth/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | const ProtectedRoute = props => { 6 | const user = useSelector(store => store.session.user) 7 | return ( 8 | 9 | {(user)? props.children : } 10 | 11 | ) 12 | }; 13 | 14 | 15 | export default ProtectedRoute; 16 | -------------------------------------------------------------------------------- /app/forms/review_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, TextField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class ReviewForm(FlaskForm): 7 | count = IntegerField('Count', validators=[DataRequired()]) 8 | content = TextField('Content', validators=[DataRequired()]) 9 | user_id = IntegerField('User ID', validators=[DataRequired()]) 10 | spot_id = IntegerField('Spot ID', validators=[DataRequired()]) 11 | -------------------------------------------------------------------------------- /app/models/location.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class Location(db.Model): 5 | __tablename__ = "locations" 6 | 7 | id = db.Column(db.Integer, nullable=False, primary_key=True) 8 | name = db.Column(db.String(100), nullable=False) 9 | 10 | spots = db.relationship( 11 | "Spot", back_populates="location") 12 | 13 | def to_dict(self): 14 | return { 15 | "id": self.id, 16 | "name": self.name 17 | } 18 | -------------------------------------------------------------------------------- /react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import './index.css'; 5 | import App from './App'; 6 | import configureStore from './store'; 7 | 8 | const store = configureStore(); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /app/forms/booking_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, DateField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class BookingForm(FlaskForm): 7 | spot_id = IntegerField('Spot', validators=[DataRequired()]) 8 | user_id = IntegerField('User', validators=[DataRequired()]) 9 | start_date = DateField('Start Date', validators=[DataRequired()]) 10 | end_date = DateField('End Date', validators=[DataRequired()]) 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/forms/spot_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField, TextField, SubmitField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class SpotForm(FlaskForm): 7 | name = StringField('Name', validators=[DataRequired()]) 8 | description = TextField('Description', validators=[DataRequired()]) 9 | location_id = IntegerField('Location', validators=[DataRequired()]) 10 | pet_friendly = BooleanField('Pet Friendly', validators=[]) 11 | private = BooleanField('Private', validators=[DataRequired()]) 12 | available = BooleanField('Available', validators=[]) 13 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { logout } from "../../store/session"; 4 | 5 | const LogoutButton = () => { 6 | const dispatch = useDispatch(); 7 | const onLogout = async (e) => { 8 | await dispatch(logout()); 9 | }; 10 | 11 | return ( 12 | 18 | ); 19 | }; 20 | 21 | export default LogoutButton; 22 | -------------------------------------------------------------------------------- /app/models/image.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | class Image(db.Model): 4 | __tablename__ = "images" 5 | 6 | id = db.Column(db.Integer, primary_key = True) 7 | image_url = db.Column(db.Text, nullable = False) 8 | spot_id = db.Column(db.Integer, db.ForeignKey("spots.id")) 9 | review_id = db.Column(db.Integer, db.ForeignKey("reviews.id")) 10 | 11 | spot = db.relationship("Spot", back_populates="images") 12 | review = db.relationship("Review", back_populates="images") 13 | 14 | 15 | def to_dict(self): 16 | return { 17 | "id": self.id, 18 | "image_url": self.image_url, 19 | "spot_id": self.spot_id, 20 | "review_id": self.review_id, 21 | } 22 | -------------------------------------------------------------------------------- /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://house-hopping.herokuapp.com/ 8 | 9 | # Build our React App 10 | RUN npm install 11 | RUN npm run build 12 | 13 | FROM python:3.8 14 | 15 | # Setup Flask environment 16 | ENV FLASK_APP=app 17 | ENV FLASK_ENV=production 18 | ENV SQLALCHEMY_ECHO=True 19 | 20 | EXPOSE 8000 21 | 22 | WORKDIR /var/www 23 | COPY . . 24 | COPY --from=build-stage /react-app/build/* app/static/ 25 | 26 | # Install Python Dependencies 27 | RUN pip install -r requirements.txt 28 | RUN pip install psycopg2 29 | 30 | # Run flask environment 31 | CMD gunicorn app:app 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements 6 | # 7 | 8 | -i https://pypi.org/simple 9 | alembic==1.4.3 10 | click==7.1.2 11 | dnspython==2.1.0 12 | email-validator==1.1.3 13 | faker==8.8.1 14 | flask-cors==3.0.8 15 | flask-jwt-extended==3.24.1 16 | flask-login==0.5.0 17 | flask-migrate==2.5.3 18 | flask-sqlalchemy==2.4.4 19 | flask-wtf==0.14.3 20 | flask==1.1.2 21 | gunicorn==20.0.4 22 | idna==3.2 23 | itsdangerous==1.1.0 24 | jinja2==2.11.2 25 | mako==1.1.3 26 | markupsafe==1.1.1 27 | pyjwt==1.7.1 28 | python-dateutil==2.8.1 29 | python-dotenv==0.14.0 30 | python-editor==1.0.4 31 | six==1.15.0 32 | sqlalchemy==1.3.19 33 | text-unidecode==1.3 34 | werkzeug==1.0.1 35 | wtforms==2.3.3 36 | -------------------------------------------------------------------------------- /app/api/image_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import login_required, current_user 3 | from app.models import Image, db, Spot 4 | 5 | image_routes = Blueprint('images', __name__) 6 | 7 | 8 | 9 | @image_routes.route('/') 10 | def get_all_images(): 11 | images = Image.query.all() 12 | # print(images) 13 | imagesDict = {} 14 | for image in images: 15 | imagesDict[image.id] = image.to_dict() 16 | # return {"imagesDict": [image.to_dict() for image in images]} 17 | return imagesDict 18 | 19 | 20 | @image_routes.route('/spot/') 21 | def images_by_spot(id): 22 | images = Image.query.filter(Image.spot_id == id).all() 23 | # print('IMAGESSSSSS', images) 24 | imagesDict = {} 25 | for image in images: 26 | imagesDict = image.to_dict() 27 | return imagesDict 28 | -------------------------------------------------------------------------------- /app/models/booking.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | class Booking(db.Model): 4 | __tablename__ = "bookings" 5 | 6 | id = db.Column(db.Integer, primary_key = True) 7 | spot_id = db.Column(db.Integer, db.ForeignKey("spots.id"), nullable=False) 8 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"),nullable=False) 9 | start_date = db.Column(db.Date, nullable = False) 10 | end_date = db.Column(db.Date, nullable = False) 11 | 12 | user = db.relationship("User", back_populates="bookings") 13 | spot = db.relationship("Spot", lazy='subquery', back_populates="bookings") 14 | 15 | def to_dict(self): 16 | return { 17 | "id": self.id, 18 | "spot_id": self.spot_id, 19 | "user_id": self.user_id, 20 | "start_date": self.start_date, 21 | "end_date": self.end_date, 22 | "spot": [self.spot.to_dict()], 23 | } 24 | -------------------------------------------------------------------------------- /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 = "==1.1.3" 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 | dnspython = "==2.1.0" 31 | idna = "==3.2" 32 | Faker = "==8.8.1" 33 | text-unidecode = "==1.3" 34 | 35 | [dev-packages] 36 | psycopg2-binary = "==2.8.6" 37 | autopep8 = "*" 38 | pylint = "*" 39 | 40 | [requires] 41 | python_version = "3.9" 42 | -------------------------------------------------------------------------------- /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 | birth_date = StringField('birth_date', validators=[]) 21 | about_me = StringField('about_me', validators=[]) 22 | profile_url = StringField('profile_url', validators=[]) 23 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(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 | -------------------------------------------------------------------------------- /app/models/message.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | class Message(db.Model): 3 | __tablename__ = 'messages' 4 | 5 | id = db.Column(db.Integer, nullable = False, primary_key=True) 6 | user_id_sender = db.Column(db.Integer, db.ForeignKey("users.id"), nullable = False) 7 | user_id_recipient = db.Column(db.Integer, db.ForeignKey("users.id"), nullable = False) 8 | content = db.Column(db.Text, nullable= False) 9 | message_url = db.Column(db.Text, nullable= False) 10 | 11 | sender = db.relationship("User", foreign_keys=[user_id_sender], back_populates="user_sender") 12 | recipient = db.relationship("User", foreign_keys=[user_id_recipient], back_populates="user_recipient") 13 | 14 | def to_dict(self): 15 | return { 16 | "id": self.id, 17 | "user_id_sender": self.user_id_sender, 18 | "user_id_recipient": self.user_id_recipient, 19 | "content": self.content, 20 | "message_url": self.message_url, 21 | } 22 | -------------------------------------------------------------------------------- /app/models/review.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class Review(db.Model): 5 | __tablename__ = 'reviews' 6 | 7 | id = db.Column(db.Integer, nullable=False, primary_key=True) 8 | count = db.Column(db.Integer, nullable=False) 9 | content = db.Column(db.Text, nullable=False) 10 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) 11 | spot_id = db.Column(db.Integer, db.ForeignKey("spots.id"), nullable=False) 12 | 13 | user = db.relationship("User", lazy='subquery', back_populates="reviews") 14 | images = db.relationship( 15 | "Image", back_populates="review") 16 | spot = db.relationship("Spot", back_populates="reviews") 17 | 18 | def to_dict(self): 19 | return { 20 | "id": self.id, 21 | "count": self.count, 22 | "content": self.content, 23 | "user_id": self.user_id, 24 | "spot_id": self.spot_id, 25 | "user": [self.user.to_dict()] 26 | } 27 | -------------------------------------------------------------------------------- /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 | 37 | ); 38 | } 39 | export default User; 40 | -------------------------------------------------------------------------------- /react-app/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import session from "./session"; 4 | import spot from "./spot"; 5 | import booking from "./booking"; 6 | import review from "./review" 7 | import location from "./location" 8 | import image from "./image" 9 | 10 | const rootReducer = combineReducers({ 11 | session, 12 | spot, 13 | booking, 14 | review, 15 | location, 16 | image 17 | }); 18 | 19 | let enhancer; 20 | 21 | if (process.env.NODE_ENV === "production") { 22 | enhancer = applyMiddleware(thunk); 23 | } else { 24 | const logger = require("redux-logger").default; 25 | const composeEnhancers = 26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 27 | enhancer = composeEnhancers(applyMiddleware(thunk, logger)); 28 | } 29 | 30 | const configureStore = (preloadedState) => { 31 | return createStore(rootReducer, preloadedState, enhancer); 32 | }; 33 | 34 | export default configureStore; 35 | -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | from .locations import seed_locations, undo_locations 4 | from .spots import seed_spots, undo_spots 5 | from .images import seed_images, undo_images 6 | from .reviews import seed_reviews, undo_reviews 7 | from .bookings import seed_bookings, undo_bookings 8 | 9 | # Creates a seed group to hold our commands 10 | # So we can type `flask seed --help` 11 | seed_commands = AppGroup('seed') 12 | 13 | 14 | # Creates the `flask seed all` command 15 | @seed_commands.command('all') 16 | def seed(): 17 | seed_users() 18 | seed_locations() 19 | seed_spots() 20 | seed_reviews() 21 | seed_bookings() 22 | seed_images() 23 | # Add other seed functions here 24 | 25 | 26 | # Creates the `flask seed undo` command 27 | @seed_commands.command('undo') 28 | def undo(): 29 | undo_reviews() 30 | undo_images() 31 | undo_bookings() 32 | undo_spots() 33 | undo_locations() 34 | undo_users() 35 | # Add other undo functions here 36 | -------------------------------------------------------------------------------- /react-app/src/components/DeleteReview/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { deleteReview } from '../../store/review'; 5 | 6 | 7 | function DeleteReview({ props }) { 8 | // console.log(props.review) 9 | const dispatch = useDispatch(); 10 | const history = useHistory() 11 | 12 | async function handleOnSubmit() { 13 | if (props.review.id) { 14 | await dispatch(deleteReview(props.review.id)); 15 | } 16 | // history.push(`/`) 17 | // history.push(`/spots/${props.review.spot_id}`) 18 | window.location.reload(false); 19 | } 20 | 21 | return ( 22 |
23 | 26 |
27 | ) 28 | } 29 | 30 | export default DeleteReview; 31 | -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | House Hopping 6 | 7 | 8 | 9 | 10 | 15 | 18 | 19 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /react-app/src/services/auth.js: -------------------------------------------------------------------------------- 1 | export const authenticate = async() => { 2 | const response = await fetch('/api/auth/',{ 3 | headers: { 4 | 'Content-Type': 'application/json' 5 | } 6 | }); 7 | return await response.json(); 8 | } 9 | 10 | export const login = async (email, password) => { 11 | const response = await fetch('/api/auth/login', { 12 | method: 'POST', 13 | headers: { 14 | 'Content-Type': 'application/json' 15 | }, 16 | body: JSON.stringify({ 17 | email, 18 | password 19 | }) 20 | }); 21 | return await response.json(); 22 | } 23 | 24 | export const logout = async () => { 25 | const response = await fetch("/api/auth/logout", { 26 | headers: { 27 | "Content-Type": "application/json", 28 | } 29 | }); 30 | return await response.json(); 31 | }; 32 | 33 | 34 | export const signUp = async (username, email, password) => { 35 | const response = await fetch("/api/auth/signup", { 36 | method: "POST", 37 | headers: { 38 | "Content-Type": "application/json", 39 | }, 40 | body: JSON.stringify({ 41 | username, 42 | email, 43 | password, 44 | }), 45 | }); 46 | return await response.json(); 47 | } -------------------------------------------------------------------------------- /react-app/src/store/location.js: -------------------------------------------------------------------------------- 1 | /* Constants */ 2 | 3 | const LOAD_LOCATIONS = 'locations/LOAD_LOCATIONS'; 4 | 5 | 6 | /* Action Creator */ 7 | 8 | const loadLocationsActionCreator = (locations) => ({ 9 | type: LOAD_LOCATIONS, 10 | payload: locations 11 | }) 12 | 13 | 14 | /* Thunk */ 15 | 16 | export const fetchLocations = () => async (dispatch) => { 17 | const response = await fetch(`/api/locations/`, { 18 | method: "GET", 19 | headers: { 20 | "Content-Type": "application/json", 21 | }, 22 | }); 23 | const responseObject = await response.json(); 24 | if (responseObject.errors) { 25 | return responseObject; 26 | } 27 | // console.log(responseObject, "responseObject🙂"); 28 | dispatch(loadLocationsActionCreator(responseObject)); 29 | } 30 | 31 | /* Reducer */ 32 | const initialState = { locations:[]} 33 | 34 | export default function reducer(state = initialState, action) { 35 | let newState; 36 | 37 | switch (action.type) { 38 | 39 | case LOAD_LOCATIONS: 40 | newState = { ...state,locations:{...state.locations} }; 41 | newState.locations = action.payload; 42 | return newState; 43 | 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "http-proxy-middleware": "^1.0.5", 10 | "postcss": "^8.3.5", 11 | "react": "^17.0.0", 12 | "react-datalist-input": "^2.2.1", 13 | "react-dates": "^21.8.0", 14 | "react-dom": "^17.0.0", 15 | "react-redux": "^7.2.4", 16 | "react-router-dom": "^5.2.0", 17 | "react-scripts": "3.4.3", 18 | "redux": "^4.1.0", 19 | "redux-logger": "^3.0.6", 20 | "redux-thunk": "^2.3.0", 21 | "tailwindcss": "^2.2.2" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "proxy": "http://localhost:5000" 45 | } 46 | -------------------------------------------------------------------------------- /_work/movedProjectWork.txt: -------------------------------------------------------------------------------- 1 | ## Steps to reactivate your projects after you moved thr root directory ## 2 | 3 | -[x] delete the .venv folder 4 | 5 | 6 | -[] pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt 7 | 8 | 9 | 10 | ## Seed Data Work ## 11 | 12 | CREATE USER assign_now_app WITH PASSWORD '1SuperSecretPassword' CREATEDB; 13 | CREATE DATABASE assign_now_app_db with OWNER assign_now_app; 14 | DATABASE_URL=postgresql://assign_now_app:1SuperSecretPassword@localhost/assign_now_app_db 15 | 16 | CREATE USER starter_redux_user WITH PASSWORD 'password' CREATEDB; 17 | CREATE DATABASE starter_redux with OWNER starter_redux_user; 18 | 19 | -[x] create a migration repository with the following command: 20 | flask db init 21 | 22 | -[x] generate an initial migration 23 | flask db migrate 24 | 25 | -[] apply the migration to the database 26 | flask db upgrade 27 | 28 | -[] flask seed all 29 | 30 | 31 | -[] flask db --help 32 | 33 | if you have an issue and you need to edit or add an attribute into a MODEL for sqlAlchemy, 34 | 35 | run the following: 36 | 37 | flask seed undo 38 | flask db downgrade 39 | 40 | --- Delete the versions inside of the versions folder inside the migration folder 41 | 42 | flask db migrate 43 | flask db upgrade 44 | 45 | flask seed all 46 | -------------------------------------------------------------------------------- /app/api/spot_search_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import login_required, current_user 3 | from app.models import Booking, Spot, db 4 | from app.forms import SpotSearchForm 5 | from datetime import date, datetime 6 | 7 | spot_search_routes = Blueprint('spot-search', __name__) 8 | 9 | 10 | @spot_search_routes.route('///') 11 | def reviews_by_spot(location, start_date, end_date): 12 | # print('🌴location', location, type(location)) 13 | # formatted_start_date = datetime.strptime(start_date, "%d/%m/%y") 14 | # formatted_end_date = datetime.strftime(end_date, "%d/%m/%y") 15 | 16 | conflictedBookings = Booking.query.filter(Booking.start_date <= end_date) \ 17 | .filter(Booking.end_date >= start_date).all() 18 | 19 | conflictedBookingsSpotIdSet = set() 20 | for booking in conflictedBookings: 21 | conflictedBookingsSpotIdSet.add(booking.spot_id) 22 | # print("🙂dir(Spot)", dir(Spot)) 23 | availableSpots = Spot.query.filter( 24 | Spot.id.notin_(conflictedBookingsSpotIdSet)) \ 25 | .filter(Spot.location_id == location).all() 26 | 27 | availableSpotsDict = {} 28 | for spot in availableSpots: 29 | availableSpotsDict[spot.id] = spot.to_dict() 30 | return availableSpotsDict 31 | -------------------------------------------------------------------------------- /react-app/src/store/image.js: -------------------------------------------------------------------------------- 1 | const LOAD_IMAGES ="images/LOAD_IMAGES"; 2 | 3 | 4 | const loadImage = (images) => ({ 5 | type:LOAD_IMAGES, 6 | payload: images 7 | }); 8 | 9 | 10 | 11 | export const getImagesBySpotId = (spot_id) => async (dispatch) => { 12 | const response = await fetch(`/api/images/spot/${spot_id}`); 13 | // console.log(response) 14 | 15 | if (response.ok) { 16 | const responseObject = await response.json(); 17 | // console.log(responseObject) 18 | dispatch(loadImage(responseObject)); 19 | } 20 | } 21 | 22 | 23 | export const getAllImages = () => async (dispatch) => { 24 | const response = await fetch(`/api/images/`); 25 | // console.log(response) 26 | 27 | if (response.ok) { 28 | const responseObject = await response.json(); 29 | // console.log(responseObject) 30 | dispatch(loadImage(responseObject)); 31 | } 32 | } 33 | 34 | 35 | 36 | const initialState = {}; 37 | 38 | export default function reducer(state=initialState, action) { 39 | let newState; 40 | 41 | switch(action.type) { 42 | case LOAD_IMAGES: 43 | newState = { ...state } 44 | newState.image = action.payload 45 | // console.log(newState) 46 | return newState 47 | 48 | default: 49 | return state; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /_work/heroku.txt: -------------------------------------------------------------------------------- 1 | -[x] logged into the heroku cli 2 | 3 | heroku login (then press ENTER TWICE) 4 | 5 | 6 | 7 | -[] in local terminal & Start up Docker GUI 8 | ``` 9 | heroku container:login 10 | heroku container:push web -a house-hopping 11 | heroku container:release web -a house-hopping 12 | ``` 13 | -[] in local terminal 14 | ``` 15 | heroku run -a house-hopping flask db upgrade 16 | heroku run -a house-hopping flask seed all 17 | heroku run -a house-hopping flask db downgrade 18 | heroku run -a house-hopping flask seed undo 19 | ``` 20 | -[] in local terminal 21 | ``` 22 | heroku run -a house-hopping flask db upgrade 23 | heroku run -a house-hopping flask seed all 24 | heroku run -a house-hopping flask db downgrade 25 | heroku run -a house-hopping flask seed undo 26 | ``` 27 | if need 28 | https://hackmd.io/@jma/S1A1YjP9u 29 | 30 | ## Updating DB on heroku 31 | 32 | -[x] destroys all the tables in Heroku DB and doesn't make you have to type your app name again 33 | (wipes the PG clean) 34 | heroku pg:reset -a house-hopping --confirm house-hopping 35 | -[x] 36 | heroku run -a house-hopping flask db upgrade 37 | -[x] 38 | heroku run -a house-hopping flask seed all 39 | 40 | 41 | # Login to the heroku cointainer registry 42 | -[x] 43 | heroku container:push web -a web house-hopping 44 | -[x] 45 | heroku: container:release web -a house-hopping 46 | -------------------------------------------------------------------------------- /react-app/src/components/Splash/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Redirect, useParams } from "react-router"; 3 | import { NavLink, useHistory } from "react-router-dom"; 4 | import Spot from "../Spot"; 5 | import SearchBar from '../SearchBar'; 6 | import {useDispatch, useSelector} from 'react-redux'; 7 | import { fetchAllSpots } from "../../store/spot" 8 | import { fetchRandomSpot } from "../../store/spot"; 9 | 10 | 11 | 12 | function Splash() { 13 | const history = useHistory(); 14 | const dispatch = useDispatch(); 15 | const randomSpot = useSelector(state => state.spot.randomSpot); 16 | const current_user = useSelector(state => state.session.user); 17 | // let { id } = useParams(); 18 | // if (!id) { 19 | // id = 2; 20 | // } 21 | 22 | useEffect(() => { 23 | dispatch(fetchRandomSpot()); 24 | }, [dispatch]) 25 | 26 | useEffect(() => { 27 | if (!current_user) { 28 | history.push('/') 29 | } 30 | }, [current_user]) 31 | 32 | 33 | return ( 34 |
35 |

Welcome to House Hopping

36 |
37 | 38 |

Book These Spots!

39 | 40 |
41 | ); 42 | } 43 | 44 | export default Splash; 45 | -------------------------------------------------------------------------------- /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, nullable=False, primary_key=True) 8 | name = db.Column(db.String(100), nullable=False) 9 | description = db.Column(db.Text, nullable=False) 10 | pet_friendly = db.Column(db.Boolean) 11 | private = db.Column(db.Boolean, nullable=False) 12 | available = db.Column(db.Boolean, nullable=False) 13 | location_id = db.Column(db.Integer, db.ForeignKey( 14 | "locations.id")) 15 | user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) 16 | 17 | user = db.relationship("User", back_populates="spots") 18 | location = db.relationship( 19 | "Location", back_populates="spots") 20 | bookings = db.relationship( 21 | "Booking", back_populates="spot") 22 | images = db.relationship( 23 | "Image", back_populates="spot") 24 | reviews = db.relationship( 25 | "Review", back_populates="spot") 26 | 27 | def to_dict(self): 28 | return { 29 | "id": self.id, 30 | "name": self.name, 31 | "description": self.description, 32 | "pet_friendly": self.pet_friendly, 33 | "private": self.private, 34 | "available": self.available, 35 | "location_id": self.location_id, 36 | "user_id": self.user_id, 37 | "images": [image.to_dict() for image in self.images] 38 | } 39 | -------------------------------------------------------------------------------- /react-app/src/components/SearchBar/SearchBar.css: -------------------------------------------------------------------------------- 1 | .search-bar-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .search-bar { 8 | /* display: grid; 9 | grid-template-columns: 2fr 4fr 1fr; 10 | align-items: center; */ 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | background-color:rgb(234, 234, 241); 15 | width: 50vw; 16 | /* border: .5px solid black; */ 17 | border-radius: 25px; 18 | padding: 4px; 19 | } 20 | 21 | @media screen and (max-width: 1347px) { 22 | .search-bar { 23 | display: flex; 24 | flex-direction: column; 25 | width: 25vw; 26 | min-width: 410px; 27 | } 28 | .autocomplete-input { 29 | 30 | } 31 | } 32 | 33 | .state { 34 | font-size: 16px; 35 | } 36 | 37 | .autocomplete-div { 38 | /* margin: 0; 39 | padding: 0; 40 | background: #fff; 41 | position: relative; 42 | display: inline-block; 43 | width: 150px; 44 | vertical-align: middle; 45 | border-radius: 10px; */ 46 | /* margin-left: 5px */ 47 | } 48 | 49 | .autocomplete-input { 50 | font-weight: 200; 51 | font-size: 19px; 52 | line-height: 24px; 53 | color: #484848; 54 | background-color: #fff; 55 | width: 100%; 56 | padding: 11px 11px 9px; 57 | margin-right: 0px; 58 | border: 0; 59 | border-top: 0; 60 | border-right: 0; 61 | border-bottom: 2px solid transparent; 62 | border-left: 0; 63 | border-radius: 0; 64 | box-sizing: border-box; 65 | } 66 | 67 | .search-bar > div { 68 | padding: 4px; 69 | } 70 | 71 | #search-submit:hover { 72 | background-color: rgb(54, 113, 201); 73 | } 74 | 75 | 76 | .categories-area { 77 | position: relative; 78 | top: 20px; 79 | } -------------------------------------------------------------------------------- /react-app/src/components/Booking/index.js: -------------------------------------------------------------------------------- 1 | import React,{useEffect, useState } from 'react'; 2 | import {useDispatch, useSelector} from 'react-redux'; 3 | import { useHistory, useParams, Link} from 'react-router-dom'; 4 | import {fetchBookings} from '../../store/booking' 5 | import { fetchSpot } from '../../store/spot' 6 | 7 | 8 | export default function MyBookings() { 9 | const dispatch = useDispatch(); 10 | const bookingState = useSelector(state => state.booking.bookings); 11 | const newBookState = bookingState 12 | // console.log(Object.values(newBookState)) 13 | const spotState = useSelector(state => state.spot) 14 | // console.log(spotState, 'SPOTSTATE') 15 | // console.log('bookingState😎', bookingState['2']?.spot_id) 16 | // const spotID = bookingState['2']?.spot_id 17 | // console.log(typeof bookingState['2']) 18 | const history = useHistory() 19 | const {id} = useParams(); 20 | 21 | useEffect(() => { 22 | dispatch(fetchBookings()) 23 | // console.log(bookingState.spot_id, 'IDDDD') 24 | // if (spotID) { 25 | // dispatch(fetchSpot(spotID)) 26 | // } 27 | }, []) 28 | 29 | 30 | return ( 31 |
32 |
33 | {bookingState && Object.values(bookingState).map(booking => { 34 | return ( 35 | 36 |
37 | 38 |

{booking.spot[0].name}

39 | {booking.start_date.slice(0,17)}{} 40 | 41 |
42 | ) 43 | })} 44 |
45 | 46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /app/seeds/images.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Image 2 | 3 | # Adds a demo user, you can add other users here if you want 4 | 5 | 6 | def seed_images(): 7 | 8 | image1 = Image( 9 | image_url='https://i.imgur.com/Zbisx6O.jpeg', 10 | spot_id=1 11 | ) 12 | image2 = Image( 13 | image_url='https://i.imgur.com/V4OiZBJ.jpeg', 14 | spot_id=2 15 | ) 16 | image3 = Image( 17 | image_url='https://i.imgur.com/Mw3jdsx.jpeg', 18 | spot_id=3 19 | ) 20 | image4 = Image( 21 | image_url='https://i.imgur.com/llXnMrp.jpeg', 22 | spot_id=4 23 | ) 24 | image5 = Image( 25 | image_url='https://i.imgur.com/k0wUfka.jpeg', 26 | spot_id=5 27 | ) 28 | image6 = Image( 29 | image_url='https://i.imgur.com/YGPfvsY.jpeg', 30 | spot_id=6 31 | ) 32 | image7 = Image( 33 | image_url='https://i.imgur.com/5qYHJgJ.jpeg', 34 | spot_id=7 35 | ) 36 | image8 = Image( 37 | image_url='https://i.imgur.com/Z7H3GRb.jpeg', 38 | spot_id=8 39 | ) 40 | image9 = Image( 41 | image_url='https://i.imgur.com/dc5Nq15.jpeg', 42 | spot_id=9 43 | ) 44 | image10 = Image( 45 | image_url='https://i.imgur.com/3v5M1WI.jpeg', 46 | spot_id=10 47 | ) 48 | image11 = Image( 49 | image_url='https://i.imgur.com/bUBNNlS.jpeg', 50 | spot_id=11 51 | ) 52 | 53 | 54 | db.session.add(image1) 55 | db.session.add(image2) 56 | db.session.add(image3) 57 | db.session.add(image4) 58 | db.session.add(image5) 59 | db.session.add(image6) 60 | db.session.add(image7) 61 | db.session.add(image8) 62 | db.session.add(image9) 63 | db.session.add(image10) 64 | db.session.add(image11) 65 | 66 | db.session.commit() 67 | 68 | 69 | 70 | def undo_images(): 71 | db.session.execute('TRUNCATE images;') 72 | db.session.commit() 73 | -------------------------------------------------------------------------------- /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 | email = db.Column(db.String(255), nullable=False, unique=True) 11 | first_name = db.Column(db.String(20), nullable=False) 12 | last_name = db.Column(db.String(40), nullable=False) 13 | birth_date = db.Column(db.Date, nullable=False) 14 | about_me = db.Column(db.Text, nullable=False) 15 | is_host = db.Column(db.Boolean, nullable=False) 16 | hashed_password = db.Column(db.String(255), nullable=False) 17 | profile_url = db.Column(db.Text) 18 | 19 | spots = db.relationship("Spot", back_populates="user") 20 | bookings = db.relationship( 21 | "Booking", back_populates="user") 22 | reviews = db.relationship( 23 | "Review", back_populates="user") 24 | 25 | user_sender = db.relationship( 26 | "Message", foreign_keys="Message.user_id_sender", back_populates="sender") 27 | user_recipient = db.relationship( 28 | "Message", foreign_keys="Message.user_id_recipient", back_populates="recipient") 29 | 30 | @property 31 | def password(self): 32 | return self.hashed_password 33 | 34 | @password.setter 35 | def password(self, password): 36 | self.hashed_password = generate_password_hash(password) 37 | 38 | def check_password(self, password): 39 | return check_password_hash(self.password, password) 40 | 41 | def to_dict(self): 42 | return { 43 | "id": self.id, 44 | "email": self.email, 45 | "first_name": self.first_name, 46 | "last_name": self.last_name, 47 | "birth_date": str(self.birth_date), 48 | "about_me": self.about_me, 49 | "is_host": self.is_host, 50 | "profile_url": self.profile_url, 51 | } 52 | -------------------------------------------------------------------------------- /react-app/src/components/One_booking/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { Link, useParams } from "react-router-dom"; 4 | import {fetchBooking} from '../../store/booking' 5 | 6 | 7 | export default function OneBooking() { 8 | const dispatch = useDispatch(); 9 | const currentBooking = useSelector((state) => state.booking?.loaded_booking); 10 | // console.log(currentBooking, 'LOADED') 11 | 12 | const {id} = useParams() 13 | 14 | useEffect(() => { 15 | if (id) { 16 | dispatch(fetchBooking(id)); 17 | } 18 | }, [dispatch, id]); 19 | 20 | const newStartDate = currentBooking?.start_date?.slice(0,17) 21 | const newEndDate = currentBooking?.end_date?.slice(0,17) 22 | 23 | return ( 24 |
25 |

Update Your Booking

26 |
27 |
28 | {currentBooking?.spot && currentBooking?.spot[0]?.images?.length > 0 ? {currentBooking?.spot[0]?.name} : default house} 37 | 38 |
39 |

Current Booking:

40 |
41 | 42 | {currentBooking?.spot && currentBooking?.spot[0]?.name} 43 | 44 |
45 |
Check In: {newStartDate}
46 |
Check Out: {newEndDate}
47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/api/booking_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import login_required, current_user 3 | from app.models import Booking, User, db 4 | from app.forms import BookingForm 5 | 6 | booking_routes = Blueprint('booking', __name__) 7 | 8 | 9 | @booking_routes.route('/create', methods=['GET', 'POST']) 10 | @login_required 11 | def create_bookings(): 12 | if request.method == 'POST': 13 | #create booking 14 | form = BookingForm() 15 | booking = Booking( 16 | spot_id=form.data['spot_id'], 17 | user_id=form.data['user_id'], 18 | start_date=form.data['start_date'], 19 | end_date=form.data['end_date'], 20 | ) 21 | db.session.add(booking) 22 | db.session.commit() 23 | return booking.to_dict() #returning spot object, may use to_dict in future 24 | 25 | 26 | @booking_routes.route('/', methods=['POST', 'DELETE']) 27 | def update_delete_bookings(id): 28 | if request.method == "POST": 29 | form = BookingForm() 30 | 31 | booking = Booking.query.get(id) 32 | 33 | # booking.spot_id=1, 34 | # booking.user_id=2, 35 | booking.start_date=form.data['start_date'], 36 | booking.end_date=form.data['end_date'], 37 | 38 | db.session.commit() 39 | return booking.to_dict() 40 | elif request.method == "DELETE": 41 | booking = Booking.query.get(id) 42 | db.session.delete(booking) 43 | db.session.commit() 44 | return booking.to_dict() 45 | 46 | 47 | @booking_routes.route('/') 48 | def get_booking(id): 49 | 50 | bookings = Booking.query.get(id) 51 | print(bookings, 'BOOKINGS') 52 | 53 | return bookings.to_dict() 54 | 55 | @booking_routes.route('/') 56 | def get_bookings(): 57 | # bookings= Booking.query.all() 58 | bookings= Booking.query.filter(Booking.user_id == current_user.id).all() 59 | bookingsDict = {} 60 | # print(bookings_by_owner) 61 | # print('🎨 bookings',bookings) 62 | for booking in bookings: 63 | bookingsDict[booking.id] = booking.to_dict() 64 | return bookingsDict 65 | -------------------------------------------------------------------------------- /app/seeds/bookings.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Booking 2 | import datetime 3 | from faker import Faker 4 | 5 | fake = Faker() 6 | 7 | # Adds a demo user, you can add other users here if you want 8 | 9 | 10 | def seed_bookings(): 11 | 12 | booking1 = Booking( 13 | spot_id=1, 14 | user_id=2, 15 | start_date="2021-07-01", 16 | end_date="2021-07-05" 17 | ) 18 | booking2 = Booking( 19 | spot_id=2, 20 | user_id=1, 21 | start_date="2021-07-01", 22 | end_date="2021-07-05", 23 | ) 24 | 25 | booking3 = Booking( 26 | spot_id=3, 27 | user_id=3, 28 | start_date="2021-07-01", 29 | end_date="2021-07-05" 30 | ) 31 | booking4 = Booking( 32 | spot_id=4, 33 | user_id=4, 34 | start_date="2021-07-01", 35 | end_date="2021-07-05", 36 | ) 37 | 38 | booking5 = Booking( 39 | spot_id=5, 40 | user_id=5, 41 | start_date="2021-07-01", 42 | end_date="2021-07-05" 43 | ) 44 | booking6 = Booking( 45 | spot_id=6, 46 | user_id=6, 47 | start_date="2021-07-01", 48 | end_date="2021-07-05", 49 | ) 50 | 51 | booking7 = Booking( 52 | spot_id=7, 53 | user_id=6, 54 | start_date="2021-07-01", 55 | end_date="2021-07-05" 56 | ) 57 | booking8 = Booking( 58 | spot_id=8, 59 | user_id=4, 60 | start_date="2021-07-01", 61 | end_date="2021-07-05", 62 | ) 63 | 64 | db.session.add(booking1) 65 | db.session.add(booking2) 66 | db.session.add(booking3) 67 | db.session.add(booking4) 68 | db.session.add(booking5) 69 | db.session.add(booking6) 70 | db.session.add(booking7) 71 | db.session.add(booking8) 72 | 73 | db.session.commit() 74 | 75 | # Uses a raw SQL query to TRUNCATE the bookings table. 76 | # SQLAlchemy doesn't have a built in function to do this 77 | # TRUNCATE Removes all the data from the table, and resets 78 | # the auto incrementing primary key 79 | 80 | 81 | def undo_bookings(): 82 | db.session.execute('TRUNCATE bookings;') 83 | db.session.commit() 84 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | 5 | import LogoutButton from "./auth/LogoutButton"; 6 | import "../output.css"; 7 | 8 | const NavBar = ({pageName}) => { 9 | const user = useSelector((store) => store.session.user); 10 | // console.log(user); 11 | 12 | return ( 13 | 59 | ) 60 | }; 61 | 62 | export default NavBar; 63 | -------------------------------------------------------------------------------- /react-app/src/components/UsersList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { fetchSpotByUser, fetchSpotReviewsByUser, fetchAllSpots} from '../store/spot'; 5 | import Spot from '../components/Spot' 6 | import MyBookings from "./Booking"; 7 | 8 | 9 | export default function UsersList() { 10 | const dispatch = useDispatch(); 11 | const user = useSelector(state => state.session.user); 12 | // const spots = useSelector(state => state.spot.userReviewSpots) 13 | const spots = useSelector(state => Object.values(state.spot.spots)) 14 | // console.log('🙂 spots',spots); 15 | 16 | 17 | useEffect(() => { 18 | // console.log(user); 19 | // dispatch(fetchSpotReviewsByUser(user.id)); 20 | dispatch(fetchSpotByUser(user.id)); 21 | }, [dispatch]) 22 | 23 | 24 | return ( 25 |
26 |

Current Bookings:

27 | 28 |

Current Listings:

29 |
30 | {spots.map(spotObj => { 31 | return( 32 |
33 |
34 | {spotObj.images.length > 0 ? {spotObj.name} : default house} 43 |
44 | 45 |
{spotObj?.name}
46 |
47 |

{spotObj?.description}

48 |

{spotObj.private ? "Private Space" : "Shared Space"}

49 |

{spotObj.pet_friendly ? "Pet Friendly!" : "Sorry, no pets allowed 😢"}

50 |
51 |
52 |
53 | ) 54 | })} 55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /react-app/src/components/CreateReviewForm/index.js: -------------------------------------------------------------------------------- 1 | import React ,{useState, useEffect} from 'react' 2 | import { useParams, useHistory } from "react-router-dom"; 3 | import { useDispatch,useSelector } from "react-redux"; 4 | import {createReview} from "../../store/review" 5 | import "./createReviewForm.css" 6 | 7 | 8 | export default function CreateReviewForm() { 9 | const dispatch = useDispatch(); 10 | const [content,setContent] = useState(''); 11 | const [count,setCount] = useState(-1); 12 | const [error, setError] = useState(''); 13 | const history = useHistory(); 14 | const current_user = useSelector(state => state.session.user) 15 | const {id} = useParams() // This is the SPOT ID 16 | 17 | const handleCreateReviewFormSubmit = (event) => { 18 | event.preventDefault(); 19 | if (content === '') { 20 | setError("Please add review content") 21 | } 22 | else if (count === -1) { 23 | setError("Please select a rating") 24 | } else { 25 | setError("") 26 | dispatch(createReview(content,count, current_user.id,id)) 27 | } 28 | } 29 | return ( 30 |
31 |

Post Your Review!

32 |
33 | 34 | 35 |
36 | 37 | 45 |
46 |

{error}

47 | 48 |
49 | 50 | 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /app/seeds/reviews.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Review 2 | import datetime 3 | from faker import Faker 4 | 5 | fake = Faker() 6 | 7 | # Adds a demo user, you can add other users here if you want 8 | 9 | 10 | def seed_reviews(): 11 | 12 | review1 = Review( 13 | count=5, 14 | content="Best place west of the Mississippi river", 15 | user_id=1, 16 | spot_id=1, 17 | ) 18 | review2 = Review( 19 | count=4, 20 | content="Should be a 5 star", 21 | user_id=2, 22 | spot_id=2, 23 | ) 24 | review3 = Review( 25 | count=3, 26 | content="Will reccomend to others!", 27 | user_id=3, 28 | spot_id=3, 29 | ) 30 | review4 = Review( 31 | count=4, 32 | content="Can not wait to come back", 33 | user_id=4, 34 | spot_id=4, 35 | ) 36 | review5 = Review( 37 | count=4, 38 | content="Great Garden", 39 | user_id=5, 40 | spot_id=5, 41 | ) 42 | review6 = Review( 43 | count=4, 44 | content="Very Quiet", 45 | user_id=6, 46 | spot_id=6, 47 | ) 48 | review7 = Review( 49 | count=4, 50 | content="Spacious bathroom", 51 | user_id=6, 52 | spot_id=7, 53 | ) 54 | review8 = Review( 55 | count=4, 56 | content="Great kitchen", 57 | user_id=5, 58 | spot_id=8, 59 | ) 60 | review9 = Review( 61 | count=2, 62 | content="Very convenient location", 63 | user_id=6, 64 | spot_id=9, 65 | ) 66 | review10 = Review( 67 | count=4, 68 | content="Very Clean", 69 | user_id=2, 70 | spot_id=6, 71 | ) 72 | review11 = Review( 73 | count=4, 74 | content="Amazing Hosts", 75 | user_id=3, 76 | spot_id=5, 77 | ) 78 | 79 | db.session.add(review1) 80 | db.session.add(review2) 81 | db.session.add(review3) 82 | db.session.add(review4) 83 | db.session.add(review5) 84 | db.session.add(review6) 85 | db.session.add(review7) 86 | db.session.add(review8) 87 | db.session.add(review9) 88 | db.session.add(review10) 89 | db.session.add(review11) 90 | 91 | db.session.commit() 92 | 93 | # Uses a raw SQL query to TRUNCATE the reviews table. 94 | # SQLAlchemy doesn't have a built in function to do this 95 | # TRUNCATE Removes all the data from the table, and resets 96 | # the auto incrementing primary key 97 | 98 | 99 | def undo_reviews(): 100 | db.session.execute('TRUNCATE reviews;') 101 | db.session.commit() 102 | -------------------------------------------------------------------------------- /react-app/src/components/BookSpotForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { useParams, useHistory } from "react-router-dom"; 4 | import { createBooking } from "../../store/booking"; 5 | 6 | import { DateRangePicker } from 'react-dates'; 7 | 8 | export default function BookSpotForm() { 9 | const [startDate, setStartDate] = useState(""); 10 | const [endDate, setEndDate] = useState(""); 11 | const [focusedInput, setfocusedInput] = useState(null); 12 | const bookingState = useSelector((state) => state.booking.bookings); 13 | const user = useSelector((state) => state.session); 14 | const dispatch = useDispatch(); 15 | const history = useHistory(); 16 | const { id } = useParams(); 17 | const user_id = user?.user?.id; 18 | 19 | function convert(str) { 20 | var date = new Date(str), 21 | mnth = ("0" + (date.getMonth() + 1)).slice(-2), 22 | day = ("0" + date.getDate()).slice(-2); 23 | return [date.getFullYear(), mnth, day].join("-"); 24 | } 25 | 26 | 27 | const handleFormSubmit = async (event) => { 28 | event.preventDefault(); 29 | 30 | const startDateFormatted = convert(startDate) 31 | const endDateFormatted = convert(endDate) 32 | 33 | dispatch(createBooking(startDateFormatted, endDateFormatted, id, user_id)); 34 | history.push(`/users`); 35 | }; 36 | 37 | return ( 38 |
39 |
40 |

Book Your Vacation Today 😎

41 | { 47 | setStartDate(startDate); 48 | setEndDate(endDate); 49 | }} // PropTypes.func.isRequired, 50 | focusedInput={focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null, 51 | onFocusChange={focusedInput => setfocusedInput(focusedInput)} // PropTypes.func.isRequired, 52 | showDefaultInputIcon 53 | className="flex flex-wrap justify-center" 54 | /> 55 | 56 | 62 | 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/api/review_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import login_required, current_user 3 | from app.models import Review, db, Spot 4 | from app.forms import ReviewForm 5 | 6 | review_routes = Blueprint('reviews', __name__) 7 | 8 | # @login_required 9 | 10 | 11 | @review_routes.route('/', methods=['POST']) 12 | def create_reviews(): 13 | if request.method == 'POST': 14 | # create spot 15 | form = ReviewForm() 16 | review = Review( 17 | count=form.data['count'], 18 | content=form.data['content'], 19 | user_id=form.data['user_id'], 20 | spot_id=form.data['spot_id'], 21 | ) 22 | db.session.add(review) 23 | db.session.commit() 24 | return review.to_dict() # returning spot object, may use to_dict in future 25 | 26 | 27 | @review_routes.route('/', methods=['POST', 'DELETE']) 28 | def update_delete_reviews(id): 29 | if request.method == "POST": 30 | form = ReviewForm() 31 | review = Review.query.get(id) 32 | 33 | review.count = form.data['count'] 34 | review.content = form.data['content'] 35 | # review.user_id = form.data['user_id'], 36 | # review.spot_id = form.data['spot_id'], 37 | 38 | db.session.commit() 39 | return review.to_dict() 40 | elif request.method == "DELETE": 41 | review = Review.query.get(id) 42 | db.session.delete(review) 43 | db.session.commit() 44 | return review.to_dict() 45 | 46 | 47 | @review_routes.route('/') 48 | def single_review(id): 49 | review = Review.query.get(id) 50 | return review.to_dict() 51 | 52 | 53 | @review_routes.route('/user/') 54 | def reviews_by_user(id): 55 | # reviews = Review.query.filter(Review.user_id == id).all() 56 | reviews = Review.query.join(Spot).filter(Review.user_id == id).all() 57 | # print('reviews[0]🥳', dir(reviews[0].spot)) 58 | # print('reviews[0]🥳',reviews[0].spot.to_dict()) 59 | spotByUserReviews = {} 60 | for review in reviews: 61 | spotByUserReviews[review.spot.id] = review.spot.to_dict() 62 | # if ("reviews" not in spotByUserReviews[review.spot.id].keys()): 63 | # spotByUserReviews[review.spot.id]["reviews"] = {} 64 | 65 | # for r in review.spot.reviews: 66 | # spotByUserReviews[review.spot.id]["reviews"][r.id] = r.to_dict() 67 | 68 | return spotByUserReviews 69 | 70 | 71 | @review_routes.route('/spot/') 72 | def reviews_by_spot(id): 73 | reviews = Review.query.filter(Review.spot_id == id).all() 74 | reviewsDict = {} 75 | for review in reviews: 76 | reviewsDict[review.id] = review.to_dict() 77 | return reviewsDict 78 | -------------------------------------------------------------------------------- /react-app/src/store/session.js: -------------------------------------------------------------------------------- 1 | // constants 2 | 3 | const SET_USER = "session/SET_USER"; 4 | const REMOVE_USER = "session/REMOVE_USER"; 5 | 6 | // action creators 7 | const setUser = (user) => ({ 8 | type: SET_USER, 9 | payload: user, 10 | }); 11 | 12 | const removeUser = () => ({ 13 | type: REMOVE_USER, 14 | }); 15 | 16 | // thunks 17 | export const authenticate = () => async (dispatch) => { 18 | const response = await fetch("/api/auth/", { 19 | headers: { 20 | "Content-Type": "application/json", 21 | }, 22 | }); 23 | const data = await response.json(); 24 | if (data.errors) { 25 | return; 26 | } 27 | dispatch(setUser(data)); 28 | }; 29 | 30 | export const login = (email, password) => async (dispatch) => { 31 | const response = await fetch("/api/auth/login", { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | body: JSON.stringify({ 37 | email, 38 | password, 39 | }), 40 | }); 41 | const data = await response.json(); 42 | console.log(data) 43 | if (data.errors) { 44 | return data; 45 | } 46 | dispatch(setUser(data)); 47 | return {}; 48 | }; 49 | 50 | export const logout = () => async (dispatch) => { 51 | const response = await fetch("/api/auth/logout", { 52 | headers: { 53 | "Content-Type": "application/json", 54 | }, 55 | }); 56 | const data = await response.json(); 57 | dispatch(removeUser()); 58 | }; 59 | 60 | export const signUp = 61 | (firstName, lastName, email, birthdate, aboutMe, password) => 62 | async (dispatch) => { 63 | const response = await fetch("/api/auth/signup", { 64 | method: "POST", 65 | headers: { 66 | "Content-Type": "application/json", 67 | }, 68 | body: JSON.stringify({ 69 | first_name: firstName, 70 | last_name: lastName, 71 | email, 72 | about_me: aboutMe, 73 | birthdate, 74 | password: password, 75 | }), 76 | }); 77 | const responseObject = await response.json(); 78 | if(responseObject.errors){ 79 | return responseObject.errors 80 | } 81 | dispatch(setUser(responseObject)); 82 | }; 83 | 84 | // reducer r 85 | 86 | // reducer r 87 | const initialState = {}; 88 | 89 | export default function reducer(state = initialState, action) { 90 | let newState = {...state} 91 | switch (action.type) { 92 | case SET_USER: 93 | newState.user = action.payload 94 | return newState; 95 | case REMOVE_USER: 96 | newState = {...state,user:{...state.user}} 97 | newState.user = null; 98 | return newState; 99 | default: 100 | return state; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash 2 | from app.models import db, User 3 | import datetime 4 | from faker import Faker 5 | 6 | fake = Faker() 7 | 8 | # Adds a demo user, you can add other users here if you want 9 | def seed_users(): 10 | 11 | # demo = User(username='Demo', email='demo@aa.io', 12 | # password='password') 13 | demo1 = User( 14 | id=1, 15 | email='demo1@aa.io', 16 | first_name='Demo', 17 | last_name='User', 18 | birth_date= "2018-06-01", 19 | about_me='junior dev', 20 | is_host=False, 21 | password='password', 22 | profile_url='' 23 | ) 24 | demo2 = User( 25 | id=2, 26 | email=fake.email(), 27 | first_name=fake.first_name(), 28 | last_name=fake.last_name(), 29 | birth_date= "2018-06-01", 30 | about_me=fake.text(), 31 | is_host=False, 32 | password=fake.password(), 33 | profile_url=fake.url() 34 | ) 35 | 36 | demo3 = User( 37 | id=3, 38 | email='demo3@aa.io', 39 | first_name='Elvis', 40 | last_name='Presley', 41 | birth_date= "2018-06-01", 42 | about_me='Rock & Roll Singer', 43 | is_host=False, 44 | password='password', 45 | profile_url='' 46 | ) 47 | demo4 = User( 48 | id=4, 49 | email='demo4@aa.io', 50 | first_name='June ', 51 | last_name='Carter', 52 | birth_date= "2014-06-01", 53 | about_me='Eukelaylee Player', 54 | is_host=False, 55 | password='password', 56 | profile_url='' 57 | ) 58 | demo5 = User( 59 | id=5, 60 | email='demo5@aa.io', 61 | first_name='Johnny ', 62 | last_name='Cash', 63 | birth_date= "2015-06-01", 64 | about_me='Country Singer', 65 | is_host=False, 66 | password='password', 67 | profile_url='' 68 | ) 69 | demo6 = User( 70 | id=6, 71 | email='demo6@aa.io', 72 | first_name='Britney', 73 | last_name='Spears', 74 | birth_date= "2016-06-01", 75 | about_me='Pop Singer', 76 | is_host=False, 77 | password='password', 78 | profile_url='' 79 | ) 80 | 81 | db.session.add(demo1) 82 | db.session.add(demo2) 83 | db.session.add(demo3) 84 | db.session.add(demo4) 85 | db.session.add(demo5) 86 | db.session.add(demo6) 87 | 88 | db.session.commit() 89 | 90 | # Uses a raw SQL query to TRUNCATE the users table. 91 | # SQLAlchemy doesn't have a built in function to do this 92 | # TRUNCATE Removes all the data from the table, and resets 93 | # the auto incrementing primary key 94 | def undo_users(): 95 | db.session.execute('TRUNCATE users;') 96 | db.session.commit() 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to House Hopping 2 | *by Alexandra Bouillon, Sylvia Onwuana, Ethan Kaseff, & Jonathan Salguero* 3 | #### [House Hopping](https://house-hopping.herokuapp.com/) is a [Couchsurfing](https://www.couchsurfing.com/) clone, that allows users to book a place to stay in certain cities or as a host, share your home/room to travelers. 4 | 5 | ![Screen Shot 2021-08-11 at 11 23 40 PM](https://user-images.githubusercontent.com/69067446/129133719-530bc690-b8a5-4d6b-b9c3-dc594dc93504.png) 6 | 7 | 8 | ### Built With 9 | The project was built utilizing the following technologies: 10 | * [Python](https://www.python.org/) 11 | * [React](https://reactjs.org/) 12 | * [Docker](https://www.docker.com/) 13 | * [Flask](https://flask.palletsprojects.com/en/2.0.x/) 14 | * [Tailwind](https://tailwindcss.com/) 15 | # 16 | 17 | ## Getting started 18 | 19 | 1. Clone this repository (only this branch) 20 | 21 | ```bash 22 | git clone https://github.com/ethan-kaseff/house_hopping.git 23 | ``` 24 | 25 | 2. Install dependencies 26 | 27 | ```bash 28 | pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt 29 | ``` 30 | 31 | 3. Create a `.env` file based on the `env.example` example with the proper settings for your 32 | development environment 33 | 4. Setup your PostgreSQL user, password and database and make sure it matches your **.env** file 34 | 5. Get into your pipenv, migrate your database, seed your database, and run your flask app 35 | 36 | ```bash 37 | pipenv shell 38 | ``` 39 | 40 | ```bash 41 | flask db upgrade 42 | ``` 43 | 44 | ```bash 45 | flask seed all 46 | ``` 47 | 48 | ```bash 49 | flask run 50 | ``` 51 | 52 | 6. To run the React App in development, cd into the `react-app` directory and run: 53 | ``` 54 | npm start 55 | ``` 56 | *IMPORTANT!* 57 | If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment. 58 | You can do this by running: 59 | 60 | ```bash 61 | pipenv lock -r > requirements.txt 62 | ``` 63 | 64 | *ALSO IMPORTANT!* 65 | psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux. 66 | There is a layer in the Dockerfile that will install psycopg2 (not binary) for us. 67 | *** 68 | 69 | 70 | ## Contact 71 | 72 | * Alexandra Bouillon - [LinkedIn](https://www.linkedin.com/in/alexandrabouillon/) - 73 | * Sylvia Onwuana - [LinkedIn](https://www.linkedin.com/in/sylvia-o/) - sonwuana1@gmail.com 74 | * Ethan Kaseff - [LinkedIn](https://www.linkedin.com/in/ethankaseff/) - 75 | * Jonathan Salguero - [LinkedIn](https://www.linkedin.com/in/josalgue/) - 76 | 77 | Project Link: [https://github.com/ethan-kaseff/house_hopping](https://github.com/ethan-kaseff/house_hopping) 78 | 79 | ## Acknowledgements 80 | * [Font Awesome](https://fontawesome.com) 81 | -------------------------------------------------------------------------------- /app/api/spot_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from flask_login import login_required, current_user 3 | from app.models import Spot, db 4 | from app.forms import SpotForm 5 | import random 6 | 7 | spot_routes = Blueprint('spots', __name__) 8 | 9 | # @login_required 10 | 11 | 12 | @spot_routes.route('/create', methods=['GET', 'POST']) 13 | def create_spots(): 14 | if request.method == 'POST': 15 | # create spot 16 | form = SpotForm() 17 | spot = Spot( 18 | name=form.data['name'], 19 | description=form.data['description'], 20 | location_id=form.data['location_id'], 21 | pet_friendly=form.data['pet_friendly'], 22 | private=form.data['private'], 23 | available=True, 24 | user_id=current_user.id 25 | ) 26 | db.session.add(spot) 27 | db.session.commit() 28 | return spot.to_dict() # returning spot object, may use to_dict in future 29 | 30 | 31 | # @spot_routes.route('/', methods=['GET']) 32 | # def get_random_spots(): 33 | # spots = Spot.query.all() 34 | # spots_dict = {spot.id: spot.to_dict() for spot in spots} 35 | # id = random.choice(list(spots_dict.keys())) 36 | # return spots_dict[id] 37 | 38 | 39 | @spot_routes.route('/', methods=['GET']) 40 | def get_all_spots(): 41 | # spots = Spot.query.filter(Spot.user_id == current_user.id).all() 42 | spots = Spot.query.all() 43 | spotsDict = {} 44 | for spot in spots: 45 | spotsDict[spot.id] = spot.to_dict() 46 | return spotsDict 47 | 48 | 49 | @spot_routes.route('/users/', methods=['GET']) 50 | def get_spots_by_user(id): 51 | # print('CURRENT', current_user.id) 52 | spots = Spot.query.filter(Spot.user_id == current_user.id).all() 53 | # print(spots) 54 | spotsDict = {} 55 | for spot in spots: 56 | spotsDict[spot.id] = spot.to_dict() 57 | return spotsDict 58 | 59 | 60 | @spot_routes.route('/', methods=['GET', 'POST', 'DELETE']) 61 | def ru_spots(id): 62 | if request.method == "GET": 63 | spot = Spot.query.get(id) 64 | return spot.to_dict() 65 | elif request.method == "POST": 66 | form = SpotForm() 67 | 68 | spot = Spot.query.get(id) 69 | 70 | spot.name = form.data['name'], 71 | spot.description = form.data['description'], 72 | spot.location = form.data['location'], 73 | spot.pet_friendly = form.data['pet_friendly'], 74 | spot.private = form.data['private'], 75 | spot.available = form.data['available'], 76 | # spot.user_id=1 77 | 78 | db.session.commit() 79 | return spot.to_dict() 80 | elif request.method == "DELETE": 81 | spot = Spot.query.get(id) 82 | db.session.delete(spot) 83 | db.session.commit() 84 | return spot.to_dict() 85 | -------------------------------------------------------------------------------- /react-app/src/components/ReviewEditForm/index.js: -------------------------------------------------------------------------------- 1 | import React ,{useState, useEffect} from 'react' 2 | import { useParams, useHistory } from "react-router-dom"; 3 | import { useDispatch,useSelector } from "react-redux"; 4 | import {fetchReviewById, updateReview} from "../../store/review" 5 | import "./reviewEditForm.css" 6 | 7 | 8 | export default function ReviewEditForm({props}) { 9 | // console.log(props) 10 | // const {review_id} = useParams(); 11 | // console.log(review_id) 12 | const dispatch = useDispatch(); 13 | const review = useSelector(state => state.review.selected_review) 14 | const [content,setContent] = useState(''); 15 | const [count,setCount] = useState(); 16 | const history = useHistory(); 17 | const [show, setShow] = useState(true); 18 | 19 | const handleShowAndClose = () => { 20 | if (show) { 21 | setShow(false) 22 | } else { 23 | setShow(true) 24 | } 25 | } 26 | 27 | 28 | const handleReviewEditFormSubmit = (event) => { 29 | event.preventDefault(); 30 | dispatch(updateReview(props.review.id,content,count)) 31 | history.push(`/spots/${props.review.spot_id}`) 32 | } 33 | 34 | // useEffect(() => { 35 | // setContent(review.content) 36 | // }, [review]) 37 | 38 | // useEffect(() => { 39 | // dispatch(fetchReviewById(review_id)); 40 | // // if (review && review.content) { 41 | // // setContent(review.content); 42 | // // } 43 | // }, [dispatch]) 44 | 45 | 46 | return ( 47 |
48 | 51 | {/*

Review Edit Form

*/} 52 | {review && 53 | 69 | } 70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /app/seeds/locations.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Location 2 | import datetime 3 | from faker import Faker 4 | 5 | fake = Faker() 6 | 7 | # Adds a demo user, you can add other users here if you want 8 | 9 | 10 | def seed_locations(): 11 | 12 | location1 = Location( 13 | name='Kansas City' 14 | ) 15 | location2 = Location( 16 | name='South Miami' 17 | ) 18 | location3 = Location( 19 | name='Mobile' 20 | ) 21 | location4 = Location( 22 | name='Jackson Spring' 23 | ) 24 | location5 = Location( 25 | name='Folsom' 26 | ) 27 | location6 = Location( 28 | name='Kentucky City' 29 | ) 30 | location7 = Location( 31 | name='Oakland' 32 | ) 33 | location8 = Location( 34 | name='Vijayawada' 35 | ) 36 | location9 = Location( 37 | name='Provincetown' 38 | ) 39 | location10 = Location( 40 | name='New York City' 41 | ) 42 | location11 = Location( 43 | name='San Francisco' 44 | ) 45 | location12 = Location( 46 | name='Atlanta' 47 | ) 48 | location13 = Location( 49 | name='Phoenix' 50 | ) 51 | location14 = Location( 52 | name='Springfield' 53 | ) 54 | location15 = Location( 55 | name='Carson City' 56 | ) 57 | location16 = Location( 58 | name='Raleigh' 59 | ) 60 | location17 = Location( 61 | name='Olympia' 62 | ) 63 | location18 = Location( 64 | name='Santa Fe' 65 | ) 66 | location19 = Location( 67 | name='Trenton' 68 | ) 69 | location20 = Location( 70 | name='New Orleans' 71 | ) 72 | location21 = Location( 73 | name='Denver' 74 | ) 75 | location22 = Location( 76 | name='Juneau' 77 | ) 78 | 79 | db.session.add(location1) 80 | db.session.add(location2) 81 | db.session.add(location3) 82 | db.session.add(location4) 83 | db.session.add(location5) 84 | db.session.add(location6) 85 | db.session.add(location7) 86 | db.session.add(location8) 87 | db.session.add(location9) 88 | db.session.add(location10) 89 | db.session.add(location11) 90 | db.session.add(location12) 91 | db.session.add(location13) 92 | db.session.add(location14) 93 | db.session.add(location15) 94 | db.session.add(location16) 95 | db.session.add(location17) 96 | db.session.add(location18) 97 | db.session.add(location19) 98 | db.session.add(location20) 99 | db.session.add(location21) 100 | db.session.add(location22) 101 | 102 | db.session.commit() 103 | 104 | # Uses a raw SQL query to TRUNCATE the locations table. 105 | # SQLAlchemy doesn't have a built in function to do this 106 | # TRUNCATE Removes all the data from the table, and resets 107 | # the auto incrementing primary key 108 | 109 | 110 | def undo_locations(): 111 | db.session.execute('TRUNCATE locations;') 112 | db.session.commit() 113 | -------------------------------------------------------------------------------- /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.review_routes import review_routes 13 | from .api.spot_search_routes import spot_search_routes 14 | from .api.booking_routes import booking_routes 15 | from .api.location_routes import location_routes 16 | from .api.image_routes import image_routes 17 | 18 | from .seeds import seed_commands 19 | 20 | from .config import Config 21 | 22 | app = Flask(__name__) 23 | 24 | # Setup login manager 25 | login = LoginManager(app) 26 | login.login_view = 'auth.unauthorized' 27 | 28 | 29 | @login.user_loader 30 | def load_user(id): 31 | return User.query.get(int(id)) 32 | 33 | 34 | # Tell flask about our seed commands 35 | app.cli.add_command(seed_commands) 36 | 37 | app.config.from_object(Config) 38 | app.register_blueprint(user_routes, url_prefix='/api/users') 39 | app.register_blueprint(auth_routes, url_prefix='/api/auth') 40 | app.register_blueprint(spot_routes, url_prefix='/api/spots') 41 | app.register_blueprint(spot_search_routes, url_prefix='/api/spot-search') 42 | app.register_blueprint(review_routes, url_prefix='/api/reviews') 43 | app.register_blueprint(booking_routes, url_prefix='/api/bookings') 44 | app.register_blueprint(location_routes, url_prefix='/api/locations') 45 | app.register_blueprint(image_routes, url_prefix='/api/images') 46 | 47 | db.init_app(app) 48 | Migrate(app, db) 49 | 50 | # Application Security 51 | CORS(app) 52 | 53 | # Since we are deploying with Docker and Flask, 54 | # we won't be using a buildpack when we deploy to Heroku. 55 | # Therefore, we need to make sure that in production any 56 | # request made over http is redirected to https. 57 | # Well......... 58 | 59 | 60 | @app.before_request 61 | def https_redirect(): 62 | if os.environ.get('FLASK_ENV') == 'production': 63 | if request.headers.get('X-Forwarded-Proto') == 'http': 64 | url = request.url.replace('http://', 'https://', 1) 65 | code = 301 66 | return redirect(url, code=code) 67 | 68 | 69 | @app.after_request 70 | def inject_csrf_token(response): 71 | response.set_cookie('csrf_token', 72 | generate_csrf(), 73 | secure=True if os.environ.get( 74 | 'FLASK_ENV') == 'production' else False, 75 | samesite='Strict' if os.environ.get( 76 | 'FLASK_ENV') == 'production' else None, 77 | httponly=True) 78 | return response 79 | 80 | 81 | @app.route('/', defaults={'path': ''}) 82 | @app.route('/') 83 | def react_root(path): 84 | print("path", path) 85 | if path == 'favicon.ico': 86 | return app.send_static_file('favicon.ico') 87 | return app.send_static_file('index.html') 88 | -------------------------------------------------------------------------------- /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/SearchResults/index.js: -------------------------------------------------------------------------------- 1 | import React,{useEffect} from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { Link, NavLink, useHistory } from "react-router-dom"; 4 | import Spot from "../Spot"; 5 | import { fetchSpot } from "../../store/spot" 6 | 7 | function SearchResults() { 8 | const dispatch = useDispatch() 9 | const availableSpots = useSelector((state) => Object.values(state.spot.availableSpots)); 10 | // console.log(availableSpots) 11 | const current_user = useSelector(state => state.session.user); 12 | const history = useHistory(); 13 | const spotArr = []; 14 | 15 | for (const key in availableSpots) { 16 | spotArr.push(availableSpots[key]); 17 | } 18 | 19 | useEffect(() => { 20 | if (!current_user){ 21 | history.push('/') 22 | } 23 | dispatch(fetchSpot(availableSpots[0]?.id)) 24 | }, [current_user, dispatch]) 25 | 26 | // Card Click Function 27 | 28 | return ( 29 | <> 30 |
31 |
32 |
33 |
34 |
35 |

Search Results:

36 |
37 |
38 | {availableSpots && ( 39 |
40 | {/* {spotArr.length > 0 ? spotArr.map((spot) => { 41 | return ( 42 | 43 | 44 | 45 | ); */} 46 | {availableSpots.length > 0 ? availableSpots.map(spot => { 47 | return ( 48 |
49 |
50 | {spot?.images ? {spot?.name} 54 | : Sunset in the mountains} 59 | 60 |
61 | 62 |
{spot?.name}
63 | 64 |

{spot?.description}

65 | {/*

{pets}

66 |

{privy}

*/} 67 | {/*

Pet Friendly {spotState.pet_friendly}

*/} 68 |
69 |
70 |
71 | )}) : 72 |
73 |

74 | Unfortunately, we currently don't have any spots in that location. 75 |

76 |
} 77 |
78 | )} 79 |
80 |
81 |
82 |
83 |
84 | 85 | ); 86 | } 87 | 88 | export default SearchResults; 89 | -------------------------------------------------------------------------------- /react-app/src/components/Spot/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { Link, useParams } from "react-router-dom"; 4 | import { fetchSpot, fetchAllSpots } from "../../store/spot"; 5 | 6 | export default function Spot({ spot }) { 7 | const dispatch = useDispatch(); 8 | const loadedSpot = useSelector((state) => state.spot.loaded_spot); 9 | const allSpotState = useSelector((state) => Object.values(state.spot.spots)); 10 | // console.log(allSpotState) 11 | 12 | // let spotState; 13 | // let { id } = useParams(); 14 | // if (!spot) { 15 | // if (!id) { 16 | // id = 2; 17 | // } 18 | // spotState = loadedSpot; 19 | // } else { 20 | // spotState = spot; 21 | // } 22 | // console.log('ID⬇️',id) 23 | // const testDispatch = () => { 24 | 25 | useEffect(() => { 26 | // if (id !== undefined) { 27 | // dispatch(fetchSpot(id)); 28 | // } 29 | dispatch(fetchAllSpots()) 30 | }, [dispatch]); 31 | // dispatch(fetchSpot(1)) 32 | // } 33 | 34 | // useEffect(() => { 35 | // dispatch(fetchSpot(id)); 36 | // }, []); 37 | let pets; 38 | let privy; 39 | // spotState.pet_friendly ? (pets = "Pet friendly!") : (pets = "No pets, sorry"); 40 | // spotState.private ? (privy = "Private House") : (privy = "Shared Space"); 41 | 42 | return ( 43 | //
44 | //
45 | // {spotState.images ? {spotState.name} : Sunset in the mountains} 54 | 55 | //
56 | //
{spotState.name}
57 | //

{spotState.description}

58 | //

{pets}

59 | //

{privy}

60 | // {/*

Pet Friendly {spotState.pet_friendly}

*/} 61 | //
62 | //
63 | //
64 |
65 | { allSpotState.map(spotObj => { 66 | return ( 67 |
68 |
69 | {spotObj.images.length > 0 ? {spotObj.name} : default house} 78 | 79 |
80 | 81 |
{spotObj.name}
82 | 83 |

{spotObj.description}

84 | {/*

{pets}

*/} 85 |

{spotObj.private ? "Private Space" : "Shared Space"}

86 |

{spotObj.pet_friendly ? "Pet Friendly!" : "Sorry, no pets allowed 😢"}

87 |
88 |
89 |
90 | ) 91 | })} 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 3 | import { useDispatch } from "react-redux"; 4 | import LoginForm from "./components/auth/LoginForm"; 5 | import SignUpForm 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 SpotForm from "./components/SpotForm"; 11 | import SearchBar from "./components/SearchBar"; 12 | import SearchResults from "./components/SearchResults"; 13 | import Spot from "./components/Spot"; 14 | import { authenticate } from "./store/session"; 15 | import MyBookings from "./components/Booking"; 16 | import BookSpotForm from "./components/BookSpotForm"; 17 | import EditBookSpotForm from "./components/EditBookSpotForm"; 18 | import Splash from "./components/Splash"; 19 | import OneBooking from "./components/One_booking"; 20 | import SpotDetailsPage from "./components/SpotDetailsPage"; 21 | import ReviewEditForm from "./components/ReviewEditForm"; 22 | // import BookSpotForm from "./components/BookSpotForm" 23 | import "react-dates/initialize"; 24 | 25 | 26 | 27 | function App() { 28 | // const [authenticated, setAuthenticated] = useState(false); 29 | const dispatch = useDispatch(); 30 | const [loaded, setLoaded] = useState(false); 31 | useEffect(() => { 32 | (async () => { 33 | await dispatch(authenticate()); 34 | setLoaded(true); 35 | // immediately invoking asynchronous function 36 | })(); 37 | }, [dispatch]); 38 | if (!loaded) { 39 | return null; 40 | } 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | {/* 61 | */} 62 | 63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | < ReviewEditForm/> 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | {/* */} 97 | 98 |
99 |
100 | ); 101 | } 102 | export default App; 103 | -------------------------------------------------------------------------------- /app/seeds/spots.py: -------------------------------------------------------------------------------- 1 | from app.models import db, Spot 2 | import datetime 3 | from faker import Faker 4 | 5 | fake = Faker() 6 | 7 | # Adds a demo user, you can add other users here if you want 8 | 9 | 10 | def seed_spots(): 11 | 12 | spot1 = Spot( 13 | name='Cozy Downtown Couch', 14 | description='Best couch vibe in your gosh darn life! Great kitchen and laundry available.', 15 | location_id=1, 16 | pet_friendly=True, 17 | private=False, 18 | available=True, 19 | user_id=1, 20 | ) 21 | spot2 = Spot( 22 | name='Haunted House 👻', 23 | description='The spookiest.', 24 | location_id=2, 25 | pet_friendly=False, 26 | private=True, 27 | available=True, 28 | user_id=2, 29 | ) 30 | spot3 = Spot( 31 | name='Country Inn', 32 | description='Feel like home', 33 | location_id=3, 34 | pet_friendly=False, 35 | private=True, 36 | available=True, 37 | user_id=3, 38 | ) 39 | spot4 = Spot( 40 | name='Jackson Spring', 41 | description='Hotter than a pepper spout', 42 | location_id=4, 43 | pet_friendly=False, 44 | private=True, 45 | available=True, 46 | user_id=4, 47 | ) 48 | spot5 = Spot( 49 | name='Folsom Prison', 50 | description='Blues', 51 | location_id=5, 52 | pet_friendly=False, 53 | private=True, 54 | available=True, 55 | user_id=5, 56 | ) 57 | spot6 = Spot( 58 | name='Camp Ground', 59 | description='Woodsy', 60 | location_id=6, 61 | pet_friendly=False, 62 | private=True, 63 | available=True, 64 | user_id=6, 65 | ) 66 | spot7 = Spot( 67 | name='Mars', 68 | description='Toxic', 69 | location_id=7, 70 | pet_friendly=False, 71 | private=True, 72 | available=True, 73 | user_id=5, 74 | ) 75 | spot8 = Spot( 76 | name='The Abbey', 77 | description='Tall ceilings', 78 | location_id=8, 79 | pet_friendly=False, 80 | private=True, 81 | available=True, 82 | user_id=2, 83 | ) 84 | spot9 = Spot( 85 | name='Bears Town', 86 | description='Provincetown', 87 | location_id=9, 88 | pet_friendly=False, 89 | private=True, 90 | available=True, 91 | user_id=1, 92 | ) 93 | spot10 = Spot( 94 | name='Post Office Cafe', 95 | description='Provincetown', 96 | location_id=9, 97 | pet_friendly=False, 98 | private=True, 99 | available=True, 100 | user_id=6, 101 | ) 102 | spot11 = Spot( 103 | name='Upper East Side Apartment', 104 | description='Close to train', 105 | location_id=10, 106 | pet_friendly=False, 107 | private=True, 108 | available=True, 109 | user_id=6, 110 | ) 111 | 112 | db.session.add(spot1) 113 | db.session.add(spot2) 114 | db.session.add(spot3) 115 | db.session.add(spot4) 116 | db.session.add(spot5) 117 | db.session.add(spot6) 118 | db.session.add(spot7) 119 | db.session.add(spot8) 120 | db.session.add(spot9) 121 | db.session.add(spot10) 122 | db.session.add(spot11) 123 | 124 | db.session.commit() 125 | 126 | # Uses a raw SQL query to TRUNCATE the spots table. 127 | # SQLAlchemy doesn't have a built in function to do this 128 | # TRUNCATE Removes all the data from the table, and resets 129 | # the auto incrementing primary key 130 | 131 | 132 | def undo_spots(): 133 | db.session.execute('TRUNCATE spots;') 134 | db.session.commit() 135 | -------------------------------------------------------------------------------- /react-app/src/components/EditBookSpotForm/index.js: -------------------------------------------------------------------------------- 1 | import React,{useEffect, useState} from 'react'; 2 | import {useDispatch, useSelector} from 'react-redux'; 3 | import {useParams, useHistory} from 'react-router-dom'; 4 | import { updateBooking, deleteBooking} from '../../store/booking' 5 | import { fetchSpot } from '../../store/spot' 6 | import { DateRangePicker } from 'react-dates'; 7 | 8 | 9 | 10 | export default function EditBookSpotForm() { 11 | const [start_date,setStartDate] = useState(''); 12 | const [end_date, setEndDate] = useState(''); 13 | const [focusedInput, setfocusedInput] = useState(null); 14 | const bookingState = useSelector(state => state.booking.loaded_booking); 15 | // console.log(bookingState) 16 | const dispatch = useDispatch() 17 | const history = useHistory() 18 | const {id} = useParams(); 19 | 20 | function convert(str) { 21 | var date = new Date(str), 22 | mnth = ("0" + (date.getMonth() + 1)).slice(-2), 23 | day = ("0" + date.getDate()).slice(-2); 24 | return [date.getFullYear(), mnth, day].join("-"); 25 | } 26 | 27 | 28 | useEffect(() => { 29 | if (bookingState?.spot_id) { 30 | dispatch(fetchSpot(bookingState?.spot_id)) 31 | } 32 | }, [dispatch]) 33 | 34 | 35 | const handleFormSubmit = (event) => { 36 | // event.preventDefault(); 37 | // console.log(id,'ID') 38 | const startDateFormatted = convert(start_date) 39 | const endDateFormatted = convert(end_date) 40 | dispatch( updateBooking(startDateFormatted, endDateFormatted, id)); 41 | history.push(`/users`); 42 | } 43 | 44 | const handleBookingDelete = (event) => { 45 | event.preventDefault(); 46 | dispatch(deleteBooking(id)); 47 | history.push(`/users`); 48 | } 49 | return ( 50 |
51 |
52 |
53 | {/*
54 | 55 | setStartDate(e.target.value)} /> 56 |
57 |
58 | 59 | setEndDate(e.target.value)} /> 60 |
*/} 61 | 62 | { 68 | setStartDate(startDate); 69 | setEndDate(endDate); 70 | }} // PropTypes.func.isRequired, 71 | focusedInput={focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null, 72 | onFocusChange={focusedInput => setfocusedInput(focusedInput)} // PropTypes.func.isRequired, 73 | required 74 | showDefaultInputIcon 75 | className="flex flex-wrap justify-center" 76 | />{' '} 77 | 78 | 79 | 80 |
81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /react-app/src/components/auth/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { Redirect,useHistory } from "react-router-dom"; 4 | import { login } from "../../store/session"; 5 | 6 | const LoginForm = () => { 7 | const dispatch = useDispatch(); 8 | const user = useSelector((state) => state.session.user); 9 | const [errors, setErrors] = useState([]); 10 | const [email, setEmail] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const history = useHistory(); 13 | 14 | const onLogin = async (e) => { 15 | e.preventDefault(); 16 | const responseObject = await dispatch(login(email, password)); 17 | if (responseObject.errors) { 18 | setErrors(responseObject.errors); 19 | } 20 | }; 21 | 22 | const onLoginWithDemo = async (e) => { 23 | e.preventDefault(); 24 | 25 | const data = await dispatch(login('demo1@aa.io', 'password')); 26 | if (data.errors) { 27 | setErrors(data.errors); 28 | } 29 | }; 30 | 31 | const updateEmail = (e) => { 32 | setEmail(e.target.value); 33 | }; 34 | 35 | const updatePassword = (e) => { 36 | setPassword(e.target.value); 37 | }; 38 | useEffect(() => { 39 | if (user) { 40 | history.push('/home') 41 | } 42 | }, [user]) 43 | 44 | 45 | return ( 46 |
47 |
48 |

Welcome to House Hopping!

49 |
50 |
54 |
55 | {errors.map((error) => ( 56 |

{error}

57 | ))} 58 |
59 |
60 | 66 | 74 |
75 |
76 | 82 | 90 | 96 | 102 |
103 |
104 |
105 |
106 |
107 | ); 108 | }; 109 | 110 | export default LoginForm; 111 | -------------------------------------------------------------------------------- /migrations/versions/3f40e31e33ee_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3f40e31e33ee 4 | Revises: 5 | Create Date: 2021-08-11 00:24:39.848325 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3f40e31e33ee' 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('locations', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=100), nullable=False), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_table('users', 27 | sa.Column('id', sa.Integer(), nullable=False), 28 | sa.Column('email', sa.String(length=255), nullable=False), 29 | sa.Column('first_name', sa.String(length=20), nullable=False), 30 | sa.Column('last_name', sa.String(length=40), nullable=False), 31 | sa.Column('birth_date', sa.Date(), nullable=False), 32 | sa.Column('about_me', sa.Text(), nullable=False), 33 | sa.Column('is_host', sa.Boolean(), nullable=False), 34 | sa.Column('hashed_password', sa.String(length=255), nullable=False), 35 | sa.Column('profile_url', sa.Text(), nullable=True), 36 | sa.PrimaryKeyConstraint('id'), 37 | sa.UniqueConstraint('email') 38 | ) 39 | op.create_table('messages', 40 | sa.Column('id', sa.Integer(), nullable=False), 41 | sa.Column('user_id_sender', sa.Integer(), nullable=False), 42 | sa.Column('user_id_recipient', sa.Integer(), nullable=False), 43 | sa.Column('content', sa.Text(), nullable=False), 44 | sa.Column('message_url', sa.Text(), nullable=False), 45 | sa.ForeignKeyConstraint(['user_id_recipient'], ['users.id'], ), 46 | sa.ForeignKeyConstraint(['user_id_sender'], ['users.id'], ), 47 | sa.PrimaryKeyConstraint('id') 48 | ) 49 | op.create_table('spots', 50 | sa.Column('id', sa.Integer(), nullable=False), 51 | sa.Column('name', sa.String(length=100), nullable=False), 52 | sa.Column('description', sa.Text(), nullable=False), 53 | sa.Column('pet_friendly', sa.Boolean(), nullable=True), 54 | sa.Column('private', sa.Boolean(), nullable=False), 55 | sa.Column('available', sa.Boolean(), nullable=False), 56 | sa.Column('location_id', sa.Integer(), nullable=True), 57 | sa.Column('user_id', sa.Integer(), nullable=False), 58 | sa.ForeignKeyConstraint(['location_id'], ['locations.id'], ), 59 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 60 | sa.PrimaryKeyConstraint('id') 61 | ) 62 | op.create_table('bookings', 63 | sa.Column('id', sa.Integer(), nullable=False), 64 | sa.Column('spot_id', sa.Integer(), nullable=False), 65 | sa.Column('user_id', sa.Integer(), nullable=False), 66 | sa.Column('start_date', sa.Date(), nullable=False), 67 | sa.Column('end_date', sa.Date(), nullable=False), 68 | sa.ForeignKeyConstraint(['spot_id'], ['spots.id'], ), 69 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 70 | sa.PrimaryKeyConstraint('id') 71 | ) 72 | op.create_table('reviews', 73 | sa.Column('id', sa.Integer(), nullable=False), 74 | sa.Column('count', sa.Integer(), nullable=False), 75 | sa.Column('content', sa.Text(), nullable=False), 76 | sa.Column('user_id', sa.Integer(), nullable=False), 77 | sa.Column('spot_id', sa.Integer(), nullable=False), 78 | sa.ForeignKeyConstraint(['spot_id'], ['spots.id'], ), 79 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 80 | sa.PrimaryKeyConstraint('id') 81 | ) 82 | op.create_table('images', 83 | sa.Column('id', sa.Integer(), nullable=False), 84 | sa.Column('image_url', sa.Text(), nullable=False), 85 | sa.Column('spot_id', sa.Integer(), nullable=True), 86 | sa.Column('review_id', sa.Integer(), nullable=True), 87 | sa.ForeignKeyConstraint(['review_id'], ['reviews.id'], ), 88 | sa.ForeignKeyConstraint(['spot_id'], ['spots.id'], ), 89 | sa.PrimaryKeyConstraint('id') 90 | ) 91 | # ### end Alembic commands ### 92 | 93 | 94 | def downgrade(): 95 | # ### commands auto generated by Alembic - please adjust! ### 96 | op.drop_table('images') 97 | op.drop_table('reviews') 98 | op.drop_table('bookings') 99 | op.drop_table('spots') 100 | op.drop_table('messages') 101 | op.drop_table('users') 102 | op.drop_table('locations') 103 | # ### end Alembic commands ### 104 | -------------------------------------------------------------------------------- /app/api/auth_routes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from flask import Blueprint, jsonify, session, request 3 | from app.models import User, db 4 | from app.forms import LoginForm 5 | from app.forms import SignUpForm 6 | from flask_login import current_user, login_user, logout_user, login_required 7 | from datetime import datetime 8 | auth_routes = Blueprint('auth', __name__) 9 | 10 | 11 | def validation_errors_to_error_messages(validation_errors): 12 | """ 13 | Simple function that turns the WTForms validation errors into a simple list 14 | """ 15 | errorMessages = [] 16 | for field in validation_errors: 17 | for error in validation_errors[field]: 18 | errorMessages.append(f"{field} : {error}") 19 | return errorMessages 20 | 21 | 22 | @auth_routes.route('/') 23 | def authenticate(): 24 | """ 25 | Authenticates a user. 26 | """ 27 | if current_user.is_authenticated: 28 | return current_user.to_dict() 29 | return {'errors': ['Unauthorized']} 30 | 31 | 32 | @auth_routes.route('/login', methods=['POST']) 33 | def login(): 34 | """ 35 | Logs a user in 36 | """ 37 | form = LoginForm() 38 | # Get the csrf_token from the request cookie and put it into the 39 | # form manually to validate_on_submit can be used 40 | form['csrf_token'].data = request.cookies['csrf_token'] 41 | if form.validate_on_submit(): 42 | # Add the user to the session, we are logged in! 43 | user = User.query.filter(User.email == form.data['email']).first() 44 | login_user(user) 45 | return user.to_dict() 46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 47 | 48 | 49 | @auth_routes.route('/logout') 50 | def logout(): 51 | """ 52 | Logs a user out 53 | """ 54 | logout_user() 55 | return {'message': 'User logged out'} 56 | 57 | 58 | # @auth_routes.route('/signup', methods=['POST']) 59 | # def sign_up(): 60 | # """ 61 | # Creates a new user and logs them in 62 | # """ 63 | # regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" 64 | # form = SignUpForm() 65 | # form['csrf_token'].data = request.cookies['csrf_token'] 66 | # if form.validate_on_submit(): 67 | # if re.match(regex, form.data['email']): 68 | # user = User( 69 | # email=form.data['email'], 70 | # first_name=form.data['first_name'], 71 | # last_name=form.data['last_name'], 72 | # birth_date=form.data['birth_date'], 73 | # # birth_date="2016-02-17", 74 | # about_me=form.data['about_me'], 75 | # is_host=True, 76 | # password=form.data['password'], 77 | # profile_url="" 78 | # ) 79 | # db.session.add(user) 80 | # db.session.commit() 81 | # login_user(user) 82 | # return user.to_dict() 83 | # else: 84 | # return {"errors": "Unable to sign up, please review your information above."} 85 | # return {'errors': validation_errors_to_error_messages(form.errors)}, 401 86 | 87 | 88 | @auth_routes.route('/signup', methods=['POST']) 89 | def sign_up(): 90 | """ 91 | Creates a new user and logs them in 92 | """ 93 | form = SignUpForm() 94 | form['csrf_token'].data = request.cookies['csrf_token'] 95 | regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" 96 | if form.validate_on_submit(): 97 | if re.match(regex, form.data['email']): 98 | user = User( 99 | email=form.data['email'], 100 | first_name=form.data['first_name'], 101 | last_name=form.data['last_name'], 102 | birth_date="2016-02-17", 103 | about_me=form.data['about_me'], 104 | is_host=True, 105 | password=form.data['password'], 106 | profile_url="" 107 | ) 108 | db.session.add(user) 109 | db.session.commit() 110 | login_user(user) 111 | return user.to_dict() 112 | else: 113 | return {"errors": "Unable to sign up, please review your email information"} 114 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 115 | 116 | 117 | @auth_routes.route('/unauthorized') 118 | def unauthorized(): 119 | """ 120 | Returns unauthorized JSON when flask-login authentication fails 121 | """ 122 | return {'errors': ['Unauthorized']}, 401 123 | -------------------------------------------------------------------------------- /react-app/src/store/booking.js: -------------------------------------------------------------------------------- 1 | //constants 2 | const LOAD_BOOKINGS = "booking/LOAD_BOOKINGS"; 3 | const LOAD_BOOKING = "booking/LOAD_BOOKING"; 4 | const ADD_UPDATE_BOOKING = "booking/ADD_UPDATE_BOOKING"; 5 | const DELETE_BOOKING = "booking/DELETE_BOOKING"; 6 | 7 | //action creators 8 | const loadBookingsActionCreator = (booking) => ({ 9 | type: LOAD_BOOKINGS, 10 | payload: booking, 11 | }); 12 | 13 | const loadBookingActionCreator = (booking) => ({ 14 | type: LOAD_BOOKING, 15 | payload: booking, 16 | }); 17 | 18 | const addUpdateBookingActionCreator = (booking) => ({ 19 | type: ADD_UPDATE_BOOKING, 20 | payload: booking, 21 | }); 22 | 23 | const deleteBookingActionCreator = (id) => ({ 24 | type: DELETE_BOOKING, 25 | payload: id, 26 | }); 27 | 28 | //thuunks 29 | export const createBooking = 30 | (start_date, end_date, spot_id, user_id) => async (dispatch) => { 31 | const response = await fetch("/api/bookings/create", { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | body: JSON.stringify({ 37 | start_date, 38 | end_date, 39 | spot_id, 40 | user_id, 41 | }), 42 | }); 43 | 44 | const responseObject = await response.json(); 45 | 46 | if (responseObject.errors) { 47 | return responseObject; 48 | } 49 | 50 | dispatch(addUpdateBookingActionCreator(responseObject)); 51 | }; 52 | 53 | // Thunk for read All Booking api/user/:id/booking/ 54 | export const fetchBookings = () => async (dispatch) => { 55 | const response = await fetch(`/api/bookings/`, { 56 | method: "GET", 57 | headers: { 58 | "Content-Type": "application/json", 59 | }, 60 | }); 61 | const responseObject = await response.json(); 62 | if (responseObject.errors) { 63 | return responseObject; 64 | } 65 | // console.log(responseObject, "🙂"); 66 | dispatch(loadBookingsActionCreator(responseObject)); 67 | }; 68 | 69 | // Thunk for read one booking, api/user/:id/booking/:booking_id 70 | export const fetchBooking = (id) => async (dispatch) => { 71 | const response = await fetch(`/api/bookings/${id}`, { 72 | method: "GET", 73 | headers: { 74 | "Content-Type": "application/json", 75 | }, 76 | }); 77 | // console.log(response, "🙂"); 78 | 79 | const responseObject = await response.json(); 80 | if (responseObject.errors) { 81 | return responseObject; 82 | } 83 | // console.log(responseObject, "🙂"); 84 | dispatch(loadBookingActionCreator(responseObject)); 85 | }; 86 | 87 | // Thunk for update 88 | export const updateBooking = (start_date, end_date, id) => async (dispatch) => { 89 | const response = await fetch(`/api/bookings/${id}`, { 90 | method: "POST", 91 | headers: { 92 | "Content-Type": "application/json", 93 | }, 94 | body: JSON.stringify({ 95 | start_date, 96 | end_date, 97 | }), 98 | }); 99 | 100 | const responseObject = await response.json(); 101 | if (responseObject.errors) { 102 | return responseObject; 103 | } 104 | // console.log(responseObject, 'HEYYYYYYYYY') 105 | dispatch(addUpdateBookingActionCreator(responseObject)); 106 | }; 107 | 108 | // Thunk for delete 109 | 110 | export const deleteBooking = (id) => async (dispatch) => { 111 | const response = await fetch(`/api/bookings/${id}`,{ 112 | method: "DELETE", 113 | }); 114 | const responseObject = await response.json(); 115 | if (responseObject.errors) { 116 | return responseObject; 117 | } 118 | dispatch(deleteBookingActionCreator(responseObject.id)); 119 | } 120 | 121 | // Reducer 122 | // const initialState = { }; 123 | const initialState = { bookings: {}, loaded_booking: {} }; 124 | 125 | export default function reducer(state = initialState, action) { 126 | let newState; 127 | // console.log(action, 'ACTION') 128 | switch (action.type) { 129 | case LOAD_BOOKINGS: 130 | newState = { ...state }; 131 | newState.bookings = action.payload; 132 | return newState; 133 | 134 | case LOAD_BOOKING: 135 | // console.log(action.payload, '!!!!!') 136 | // newState = { ...state, bookings: { ...state.bookings } }; 137 | // newState.bookings[action.payload.id] = action.payload; 138 | newState = { ...state }; 139 | newState.loaded_booking = action.payload; 140 | // newState.bookings[action.payload.id] = action.payload; 141 | return newState; 142 | 143 | case ADD_UPDATE_BOOKING: 144 | newState = { ...state, bookings: { ...state.bookings } }; 145 | newState.bookings[action.payload.id] = action.payload; 146 | return newState; 147 | 148 | case DELETE_BOOKING: 149 | newState = { ...state, bookings: { ...state.bookings } }; 150 | delete newState.bookings[action.payload.id]; 151 | return newState; 152 | 153 | default: 154 | return state; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /react-app/src/store/review.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | const CREATE_REVIEW = "reviews/CREATE_REVIEW"; 3 | const LOAD_REVIEWS_BY_SPOTID ="reviews/LOAD_REVIEWS_BY_SPOTID"; 4 | const LOAD_REVIEW_BY_ID ="reviews/LOAD_REVIEW_BY_ID"; 5 | const UPDATE_REVIEW ="reviews/UPDATE_REVIEW"; 6 | const DELETE_REVIEW ="reviews/DELETE_REVIEW"; 7 | 8 | 9 | // Action Creators 10 | const createReviewActionCreator = (review) => ({ 11 | type:CREATE_REVIEW, 12 | payload:review, 13 | }); 14 | const loadReviewsBySpotIdActionCreator = (reviews) => ({ 15 | type:LOAD_REVIEWS_BY_SPOTID, 16 | payload:reviews, 17 | }); 18 | 19 | const loadReviewByIdActionCreator = (review) => ({ 20 | type:LOAD_REVIEW_BY_ID, 21 | payload:review, 22 | }); 23 | 24 | const updateReviewActionCreator = (review) => ({ 25 | type:UPDATE_REVIEW, 26 | payload:review, 27 | }); 28 | const deleteReviewActionCreator = (id) => ({ 29 | type:DELETE_REVIEW, 30 | payload:id, 31 | }); 32 | 33 | 34 | // Thunk 35 | 36 | export const fetchReviewsBySpotId = (spot_id) => async (dispatch) => { 37 | const response = await fetch(`/api/reviews/spot/${spot_id}`,{ 38 | method: "GET", 39 | headers: { 40 | "Content-Type": "application/json", 41 | } 42 | }); 43 | // console.log(response) 44 | 45 | if (response.ok) { 46 | const responseObject = await response.json(); 47 | // console.log(responseObject) 48 | dispatch(loadReviewsBySpotIdActionCreator(responseObject)); 49 | } 50 | // if (responseObject.errors) { 51 | // return responseObject; 52 | // } 53 | } 54 | 55 | 56 | export const fetchReviewById = (review_id) => async (dispatch) => { 57 | const response = await fetch(`/api/reviews/${review_id}`,{ 58 | method: "GET", 59 | headers: { 60 | "Content-Type": "application/json", 61 | } 62 | }); 63 | const responseObject = await response.json(); 64 | 65 | if (responseObject.errors) { 66 | return responseObject; 67 | } 68 | 69 | dispatch(loadReviewByIdActionCreator(responseObject)); 70 | } 71 | 72 | export const createReview = (content,count, user_id, spot_id) => async (dispatch) => { 73 | // console.log("count😌", count) 74 | const response = await fetch(`/api/reviews/`,{ 75 | method: "POST", 76 | headers: { 77 | "Content-Type": "application/json", 78 | }, 79 | body:JSON.stringify({ content,count , user_id, spot_id}) 80 | }); 81 | const responseObject = await response.json(); 82 | if (responseObject.errors) { 83 | return responseObject; 84 | } 85 | 86 | dispatch(createReviewActionCreator(responseObject)); 87 | } 88 | 89 | export const updateReview = (review_id,content,count) => async (dispatch) => { 90 | const response = await fetch(`/api/reviews/${review_id}`,{ 91 | method: "POST", 92 | headers: { 93 | "Content-Type": "application/json", 94 | }, 95 | body:JSON.stringify({content,count}) 96 | }); 97 | // console.log(response) 98 | const responseObject = await response.json(); 99 | if (responseObject.errors) { 100 | return responseObject; 101 | } 102 | 103 | dispatch(updateReviewActionCreator(responseObject)); 104 | } 105 | export const deleteReview = (id) => async (dispatch) => { 106 | const response = await fetch(`/api/reviews/${id}`,{ 107 | method: "DELETE", 108 | // headers: { 109 | // "Content-Type": "application/json", 110 | // } 111 | }); 112 | const responseObject = await response.json(); 113 | 114 | if (responseObject.errors) { 115 | return responseObject; 116 | } 117 | 118 | dispatch(deleteReviewActionCreator(responseObject.id)); 119 | } 120 | 121 | // Reducer 122 | 123 | const initialState = { loaded_reviews:{}, selected_review:{} }; 124 | 125 | export default function reducer(state = initialState, action) { 126 | let newState; 127 | switch(action.type) { 128 | case CREATE_REVIEW: 129 | newState = {...state,loaded_reviews:{...state.loaded_reviews}}; 130 | newState.loaded_reviews[action.payload.id] = action.payload; 131 | return newState; 132 | 133 | case LOAD_REVIEWS_BY_SPOTID: 134 | newState = {...state}; 135 | newState.loaded_reviews= action.payload; 136 | return newState; 137 | 138 | case LOAD_REVIEW_BY_ID: 139 | newState = {...state}; 140 | newState.selected_review= action.payload; 141 | return newState; 142 | 143 | case UPDATE_REVIEW: 144 | newState = {...state, loaded_reviews:{...state.loaded_reviews}}; 145 | newState.selected_review= action.payload; 146 | const id = action.payload.id; 147 | newState.loaded_reviews[id] = action.payload; 148 | return newState; 149 | 150 | case DELETE_REVIEW: 151 | newState = {...state, loaded_reviews:{...state.loaded_reviews}}; 152 | delete newState.loaded_reviews[action.payload.id] 153 | return newState; 154 | 155 | default: 156 | return state; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /react-app/src/components/SpotDetailsPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect }from 'react' 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { useParams, useHistory } from "react-router-dom"; 4 | import {fetchReviewsBySpotId, deleteReview} from "../../store/review" 5 | import BookSpotForm from "../../components/BookSpotForm" 6 | import CreateReviewForm from "../../components/CreateReviewForm" 7 | import { fetchSpot } from "../../store/spot" 8 | import { getImagesBySpotId } from "../../store/image" 9 | import ReviewEditForm from '../ReviewEditForm'; 10 | import DeleteReview from '../DeleteReview'; 11 | 12 | 13 | export default function SpotDetailsPage() { 14 | const { id } = useParams(); 15 | // console.log(id) 16 | const dispatch = useDispatch(); 17 | const history = useHistory(); 18 | const spotState = useSelector(state => state.spot.loaded_spot) 19 | // console.log(spotState) 20 | const reviews = useSelector(state => state.review.loaded_reviews) 21 | // console.log(reviews) 22 | const user = useSelector(state => state.session.user) 23 | const newImage = useSelector(state => state.image.image) 24 | // console.log(newImage) 25 | 26 | if (!user) { 27 | history.push("/"); 28 | } 29 | 30 | const generateStars = (starCount) => { 31 | // console.log(starCount) 32 | const stars = []; 33 | for (let i = 0; i < starCount;i++) { 34 | stars.push() 35 | } 36 | for( let j = 0; j < 5-starCount; j++) { 37 | stars.push() 38 | } 39 | return stars; 40 | } 41 | 42 | const handleReviewEdit = (event) => { 43 | // const id = 44 | // history.push(`/review/${event.target.id.substring(event.target.id.length-1)}`); 45 | history.push(`/review/${event.target.id.substring(event.target.id.length-1)}`); 46 | } 47 | const handleReviewDelete = (event) => { 48 | dispatch(deleteReview(event.target.id.substring(event.target.id.length-1))); 49 | } 50 | 51 | 52 | useEffect(() => { 53 | dispatch(fetchReviewsBySpotId(id)); 54 | dispatch(fetchSpot(id)) 55 | dispatch(getImagesBySpotId(id)) 56 | }, [dispatch]) 57 | 58 | 59 | return ( 60 |
61 | {!(id === "new") ? ( 62 |
63 | 64 |
65 |
66 | {newImage?.image_url ? Sunset in the mountains : default house} 75 | 76 |
77 |
{spotState.name}
78 |

{spotState.description}

79 |

{spotState.private ? "Private Space" : "Shared Space"}

80 |

{spotState.pet_friendly ? "Pet Friendly!" : "Sorry, no pets allowed 😢"}

81 |
82 |
83 |
84 |
85 | {/* */} 86 | 87 |
88 | 89 |
90 |

Reviews:

91 | {reviews && Object.values(reviews).map(review => { 92 | return
93 |

{review?.user[0]?.first_name} {review?.user[0]?.last_name}

94 | {generateStars(review.count)} 95 |

{review?.content}

96 | {review.user_id == user?.id ? 97 |
98 | {/* */} 99 | {/* */} 100 | 101 | 102 |
103 | : null } 104 |
105 | })} 106 |
107 |
108 | 109 |
110 |
111 | ): null } 112 |
113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /react-app/src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState, useEffect, useCallback, useMemo } from 'react'; 3 | import { useDispatch , useSelector} from 'react-redux'; 4 | import { useHistory } from 'react-router-dom' 5 | import { DateRangePicker } from 'react-dates'; 6 | import DataListInput from 'react-datalist-input'; 7 | import './SearchBar.css' 8 | import 'react-dates/lib/css/_datepicker.css'; 9 | import { getAvailableSpots } from '../../store/spot'; 10 | // import { saveCurrentDates } from '../../store/booking'; 11 | import { fetchLocations } from '../../store/location' 12 | function SearchBar() { 13 | const dispatch = useDispatch(); 14 | const history = useHistory(); 15 | const [startDate, setStartDate] = useState(null); 16 | const [endDate, setEndDate] = useState(null); 17 | const [focusedInput, setfocusedInput] = useState(null); 18 | // For the DataList 19 | // const [items, setItems] = useState({}); 20 | const [location, setLocation] = useState(); 21 | const locations = useSelector(state => state.location.locations.locations) 22 | function convert(str) { 23 | var date = new Date(str), 24 | mnth = ("0" + (date.getMonth() + 1)).slice(-2), 25 | day = ("0" + date.getDate()).slice(-2); 26 | return [date.getFullYear(), mnth, day].join("-"); 27 | } 28 | const handleSubmit = (e) => { 29 | e.preventDefault(); 30 | const startDateFormatted = convert(startDate) 31 | const endDateFormatted = convert(endDate) 32 | // dispatch(saveCurrentDates(startDate, endDate)) 33 | dispatch(getAvailableSpots(location.key, startDateFormatted, endDateFormatted)) 34 | history.push(`/search-results`); 35 | } 36 | useEffect(() => { 37 | dispatch(fetchLocations()) 38 | if (locations) { 39 | } 40 | }, [dispatch]) 41 | useEffect(() => { 42 | // console.log("💥 items",items) 43 | // console.log("🏡 locations",locations) 44 | }, [locations]) 45 | const items = useMemo(() =>{ 46 | if (locations) { 47 | const data = locations.map((oneItem) => ({ 48 | // required: what to show to the user 49 | label: oneItem.name, 50 | // required: key to identify the item within the array 51 | key: oneItem.id, 52 | })) 53 | return data; 54 | }}, 55 | [locations] 56 | ); 57 | // const items = [ 58 | // { 59 | // key: 'Kansas City', 60 | // label: 'Kansas City' 61 | // }, 62 | // { 63 | // key: 'South Miami', 64 | // label: 'South Miami' 65 | // }, 66 | // { 67 | // key: 'Tennessee', 68 | // label: 'Tennessee' 69 | // }, 70 | // { 71 | // key: 'Jackson', 72 | // label: 'Jackson' 73 | // }, 74 | // { 75 | // key: 'Folsom', 76 | // label: 'Folsom' 77 | // }, 78 | // { 79 | // key: 'Kentucky', 80 | // label: 'Kentucky' 81 | // }, 82 | // { 83 | // key: 'Mars', 84 | // label: 'Mars' 85 | // }, 86 | // { 87 | // key: 'England', 88 | // label: 'England' 89 | // }, 90 | // { 91 | // key: 'Cape Cod', 92 | // label: 'Cape Cod' 93 | // }, 94 | // ] 95 | const onSelect = useCallback((selectedItem) => { 96 | setLocation(selectedItem); 97 | }) 98 | return ( 99 | <> 100 |
101 |
102 | {locations ? 103 |
104 | 109 |
: null } 110 |
111 | { 117 | setStartDate(startDate); 118 | setEndDate(endDate); 119 | }} // PropTypes.func.isRequired, 120 | focusedInput={focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null, 121 | onFocusChange={focusedInput => setfocusedInput(focusedInput)} // PropTypes.func.isRequired, 122 | showDefaultInputIcon 123 | /> 124 |
125 |
126 | 132 |
133 |
134 |
135 | 136 | ) 137 | } 138 | export default SearchBar; 139 | -------------------------------------------------------------------------------- /react-app/src/components/SpotForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { useHistory } from "react-router-dom"; 5 | import { createSpot } from "../store/spot"; 6 | import DataListInput from 'react-datalist-input'; 7 | import { fetchLocations } from '../store/location' 8 | 9 | 10 | function SpotForm() { 11 | const dispatch = useDispatch(); 12 | const history = useHistory() 13 | const user = useSelector(state => state.session.user); 14 | // console.log(user) 15 | const [name, setName] = useState(""); 16 | const [description, setDescription] = useState(""); 17 | const [location, setLocation] = useState(""); 18 | const [pet_friendly, setPet_friendly] = useState(false); 19 | const [pprivate, setPprivate] = useState(false); 20 | const [available, setAvailable] = useState(true); 21 | const [error,setError] = useState(""); 22 | const locations = useSelector(state => state.location.locations.locations) 23 | 24 | if (!user) { 25 | history.push("/"); 26 | } 27 | 28 | const onSubmit = async (ev) => { 29 | ev.preventDefault(); 30 | if (name === "") { 31 | setError("Please provide a name.") 32 | } 33 | else if(description === "") { 34 | setError("Please provide a description.") 35 | } 36 | else if(location === "") { 37 | setError("Please select a location.") 38 | } 39 | else { 40 | setError("") 41 | const data = await dispatch( 42 | createSpot(name, description, location, pet_friendly, pprivate, available) 43 | ); 44 | history.push("/"); 45 | } 46 | }; 47 | 48 | useEffect(() => { 49 | dispatch(fetchLocations()) 50 | if (locations) { 51 | } 52 | }, [dispatch]) 53 | 54 | return ( 55 |
56 |

Try Hosting your Spot Today!

57 |
61 |
62 | 65 | { 70 | setName(ev.target.value); 71 | }} 72 | value={name} 73 | > 74 |
75 |
76 | 79 | { 84 | setDescription(ev.target.value); 85 | }} 86 | value={description} 87 | > 88 |
89 |
90 | 93 | {locations ? 94 |
95 | 101 |
: null } 102 |
103 |
104 | 107 | { 112 | setPet_friendly(!pet_friendly); 113 | }} 114 | value={pet_friendly} 115 | > 116 |
117 |
118 | 121 | { 126 | setPprivate(!pprivate); 127 | }} 128 | value={pprivate} 129 | > 130 |
131 |
132 | 135 | { 140 | setAvailable(!available); 141 | }} 142 | value={available} 143 | > 144 |
145 |
146 |

{error}

147 |
148 | 154 |
155 |
156 | ); 157 | //end of spotform 158 | } 159 | export default SpotForm; 160 | -------------------------------------------------------------------------------- /react-app/src/components/auth/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { Redirect, useHistory } from "react-router-dom"; 4 | import { signUp } from "../../store/session"; 5 | 6 | const SignUpForm = () => { 7 | const dispatch = useDispatch(); 8 | const user = useSelector((state) => state.session.user); 9 | const [email, setEmail] = useState(""); 10 | const [firstName, setFirstName] = useState(""); 11 | const [lastName, setLastName] = useState(""); 12 | const [birthdate, setBirthdate] = useState(""); 13 | const [aboutMe, setAboutMe] = useState(""); 14 | const [password, setPassword] = useState(""); 15 | const [repeatPassword, setRepeatPassword] = useState(""); 16 | const [errors,setErrors] = useState("") 17 | const history = useHistory(); 18 | 19 | const onSignUp = async (e) => { 20 | e.preventDefault(); 21 | if (password === repeatPassword) { 22 | const e = await dispatch(signUp(firstName, lastName, email, birthdate, aboutMe, password)); 23 | if (e) { 24 | setErrors(e) 25 | } 26 | else{ 27 | setErrors('') 28 | } 29 | } else { 30 | setErrors('Password and Repeat Password must match!') 31 | } 32 | }; 33 | 34 | const updateEmail = (e) => { 35 | setEmail(e.target.value); 36 | }; 37 | const updateFirstName = (e) => { 38 | setFirstName(e.target.value); 39 | }; 40 | const updateLastName = (e) => { 41 | setLastName(e.target.value); 42 | }; 43 | const updateBirthdate = (e) => { 44 | setBirthdate(e.target.value); 45 | }; 46 | const updateAboutMe = (e) => { 47 | setAboutMe(e.target.value); 48 | }; 49 | const updatePassword = (e) => { 50 | setPassword(e.target.value); 51 | }; 52 | 53 | const updateRepeatPassword = (e) => { 54 | setRepeatPassword(e.target.value); 55 | }; 56 | 57 | useEffect(() => { 58 | if (user) { 59 | history.push('/home') 60 | } 61 | }, [user]) 62 | 63 | return ( 64 |
65 |
66 |
70 |
71 | 74 | 82 |
83 |
84 | 87 | 95 |
96 |
97 | 100 | 108 |
109 |
110 | 113 | 121 |
122 |
123 | 126 | 134 |
135 |
136 | 139 | 147 |
148 |
149 | 152 | 161 |
162 | {errors? 163 |

{errors}

164 | :null} 165 | 171 |
172 |
173 |
174 | ); 175 | }; 176 | 177 | export default SignUpForm; 178 | -------------------------------------------------------------------------------- /react-app/src/store/spot.js: -------------------------------------------------------------------------------- 1 | //constants 2 | const LOAD_SINGLE_SPOT = "spot/LOAD_SINGLE_SPOT"; 3 | const LOAD_ALL_SPOTS = "spot/LOAD_ALL_SPOTS"; 4 | const LOAD_SPOTS_BY_USER = "spot/LOAD_SPOTS_BY_USER" 5 | const LOAD_RANDOM_SPOT = "spot/LOAD_RANDOM_SPOT"; 6 | const ADD_UPDATE_SPOT = "spot/ADD_UPDATE_SPOT"; 7 | const DELETE_SPOT = "spot/DELETE_SPOT" 8 | const LOAD_AVAILABLE_SPOTS= 'spot/LOAD_AVAILABLE/SPOTS' 9 | const LOAD_SPOTS_BY_USER_REVIEWS = 'spot/LOAD_SPOTS_BY_USER_REVIEWS' 10 | 11 | 12 | //action creators 13 | const loadSingleSpotActionCreator = (spot) => ({ 14 | type: LOAD_SINGLE_SPOT, 15 | payload: spot, 16 | }); 17 | 18 | const loadAllSpotsActionCreator = (spot) => ({ 19 | type: LOAD_ALL_SPOTS, 20 | payload: spot, 21 | }); 22 | 23 | const loadHostSpots = (spot) => ({ 24 | type: LOAD_SPOTS_BY_USER, 25 | payload: spot 26 | }) 27 | 28 | const loadRandomSpotActionCreator = (spot) => ({ 29 | type: LOAD_RANDOM_SPOT, 30 | payload: spot, 31 | }); 32 | 33 | const addUpdateSpotActionCreator = (spot) => ({ 34 | type: ADD_UPDATE_SPOT, 35 | payload: spot, 36 | }); 37 | 38 | const deleteSpotActionCreator = (spot) => ({ 39 | type:DELETE_SPOT, 40 | payload:spot.id 41 | }) 42 | 43 | const loadAvailableSpots = (spots) => ({ 44 | type: LOAD_AVAILABLE_SPOTS, 45 | payload: spots 46 | }) 47 | const loadSpotsByUserReviewsActionCreator = (spots) => ({ 48 | type: LOAD_SPOTS_BY_USER_REVIEWS, 49 | payload: spots 50 | }) 51 | 52 | //thuunks 53 | export const createSpot = 54 | (name, description, location_id, pet_friendly, pprivate, available) => 55 | async (dispatch) => { 56 | 57 | const response = await fetch("/api/spots/create", { 58 | method: "POST", 59 | headers: { 60 | "Content-Type": "application/json", 61 | }, 62 | body: JSON.stringify({ 63 | name, 64 | description, 65 | location_id, 66 | pet_friendly, 67 | private: pprivate, 68 | available, 69 | }), 70 | }); 71 | 72 | const responseObject = await response.json(); 73 | 74 | if (responseObject.errors) { 75 | return responseObject; 76 | } 77 | 78 | dispatch(addUpdateSpotActionCreator(responseObject)); 79 | }; 80 | 81 | // Thunk for read 82 | export const fetchSpot =(id) => async (dispatch) => { 83 | // console.log(id) 84 | const response = await fetch(`/api/spots/${id}`, { 85 | method: "GET", 86 | headers: { 87 | "Content-Type": "application/json", 88 | }, 89 | }); 90 | const responseObject = await response.json(); 91 | if (responseObject.errors) { 92 | return responseObject; 93 | } 94 | dispatch(loadSingleSpotActionCreator(responseObject)); 95 | }; 96 | 97 | export const fetchAllSpots =() => async (dispatch) => { 98 | const response = await fetch(`/api/spots/`, { 99 | method: "GET", 100 | headers: { 101 | "Content-Type": "application/json", 102 | }, 103 | }); 104 | const responseObject = await response.json(); 105 | // console.log('😎responseObject',responseObject) 106 | if (responseObject.errors) { 107 | return responseObject; 108 | } 109 | dispatch(loadAllSpotsActionCreator(responseObject)); 110 | }; 111 | 112 | 113 | export const fetchSpotByUser = (id) => async (dispatch) => { 114 | const response = await fetch(`api/spots/users/${id}`) 115 | 116 | const responseObject = await response.json() 117 | if (responseObject.errors) { 118 | return responseObject 119 | } 120 | dispatch(loadHostSpots(responseObject)) 121 | } 122 | 123 | export const fetchRandomSpot =() => async (dispatch) => { 124 | const response = await fetch(`/api/spots/`, { 125 | method: "GET", 126 | headers: { 127 | "Content-Type": "application/json", 128 | }, 129 | }); 130 | const responseObject = await response.json(); 131 | if (responseObject.errors) { 132 | return responseObject; 133 | } 134 | dispatch(loadRandomSpotActionCreator(responseObject)); 135 | }; 136 | 137 | 138 | // Thunk for update 139 | export const updateSpot = 140 | (name, description, location, pet_friendly, pprivate, available, id) => 141 | async (dispatch) => { 142 | const response = await fetch(`/api/spots/${id}`, { 143 | method: "POST", 144 | headers: { 145 | "Content-Type": "application/json", 146 | }, 147 | body: JSON.stringify({ 148 | name, 149 | description, 150 | location, 151 | pet_friendly, 152 | private: pprivate, 153 | available, 154 | }), 155 | }); 156 | 157 | const responseObject = await response.json(); 158 | if (responseObject.errors) { 159 | return responseObject; 160 | } 161 | dispatch(addUpdateSpotActionCreator(responseObject)); 162 | 163 | }; 164 | 165 | // Thunk for delete 166 | export function deleteSpot( id ) { 167 | return async function (dispatch) { 168 | const res = await fetch(`/api/spots/${id}`, { 169 | method: 'DELETE',headers: { 170 | "Content-Type": "application/json", 171 | }, 172 | body: JSON.stringify({id}), 173 | }); 174 | if (res.ok) { 175 | 176 | const responseObject = await res.json(); 177 | // console.log(responseObject, '🙂') 178 | dispatch(deleteSpotActionCreator(responseObject)); 179 | return responseObject; 180 | } else { 181 | throw res; 182 | } 183 | } 184 | } 185 | 186 | // Thunk for Search Bar 187 | export const getAvailableSpots = (location, start_date, end_date) => async (dispatch) => { 188 | const res = await fetch(`api/spot-search/${location}/${start_date}/${end_date}`) 189 | 190 | const responseObject = await res.json(); 191 | if (responseObject.errors) { 192 | return responseObject; 193 | } 194 | 195 | dispatch(loadAvailableSpots(responseObject)); 196 | } 197 | 198 | 199 | // Thunk To Get All Reviews of the user by spot 200 | // export const fetchSpotReviewsByUser =(id) => async (dispatch) => { 201 | // const response = await fetch(`/api/reviews/user/${id}`, { 202 | // method: "GET", 203 | // headers: { 204 | // "Content-Type": "application/json", 205 | // }, 206 | // }); 207 | // const responseObject = await response.json(); 208 | // if (responseObject.errors) { 209 | // return responseObject; 210 | // } 211 | // dispatch(loadSpotsByUserReviewsActionCreator(responseObject)); 212 | // }; 213 | 214 | // Reducer 215 | const initialState = {availableSpots:{}, spots:{}, loaded_spot:{}, userReviewSpots:{}, randomSpot:{}}; 216 | 217 | 218 | export default function reducer(state = initialState, action) { 219 | let newState; 220 | // console.log(action.payload); 221 | 222 | switch (action.type) { 223 | 224 | case LOAD_AVAILABLE_SPOTS: 225 | newState = {...state}; 226 | newState.availableSpots = action.payload; 227 | return newState; 228 | 229 | case LOAD_ALL_SPOTS: 230 | newState = {...state}; 231 | newState.spots = action.payload; 232 | return newState; 233 | 234 | case LOAD_SPOTS_BY_USER: 235 | newState = {...state}; 236 | newState.spots = action.payload; 237 | return newState; 238 | 239 | 240 | case LOAD_RANDOM_SPOT: 241 | newState = {...state}; 242 | newState.randomSpot = action.payload; 243 | return newState; 244 | 245 | case LOAD_SINGLE_SPOT: 246 | newState = { ...state }; 247 | // newState.spot = action.payload; 248 | newState.loaded_spot = action.payload; 249 | return newState; 250 | 251 | case ADD_UPDATE_SPOT: 252 | newState = { ...state, spots:{...state.spots}}; 253 | newState.spots[action.payload.id] = action.payload; 254 | return newState; 255 | 256 | case DELETE_SPOT: 257 | newState = { ...state, spots:{...state.spots} }; 258 | delete newState.spots[action.payload.id] 259 | return newState; 260 | 261 | case LOAD_SPOTS_BY_USER_REVIEWS: 262 | newState = {...state}; 263 | newState.userReviewSpots = action.payload; 264 | return newState; 265 | 266 | default: 267 | return state; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "2f8368a8c2de30e0bbe7c3b5086712a59eef5e9f6be41eeec5d1b3a4b8462f8d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", 22 | "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.3" 26 | }, 27 | "click": { 28 | "hashes": [ 29 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 30 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 31 | ], 32 | "index": "pypi", 33 | "version": "==7.1.2" 34 | }, 35 | "dnspython": { 36 | "hashes": [ 37 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", 38 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" 39 | ], 40 | "index": "pypi", 41 | "version": "==2.1.0" 42 | }, 43 | "email-validator": { 44 | "hashes": [ 45 | "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b", 46 | "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7" 47 | ], 48 | "index": "pypi", 49 | "version": "==1.1.3" 50 | }, 51 | "faker": { 52 | "hashes": [ 53 | "sha256:ccd76cd86a49f1042811faaa3a7d1b094fcf8e60a1ec286190417bbb5a3f2f76", 54 | "sha256:cda50f6afaa4075464d7500ac838ec3cac3cc6824297e4340b2a17a62dc086a8" 55 | ], 56 | "index": "pypi", 57 | "version": "==8.8.1" 58 | }, 59 | "flask": { 60 | "hashes": [ 61 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 62 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 63 | ], 64 | "index": "pypi", 65 | "version": "==1.1.2" 66 | }, 67 | "flask-cors": { 68 | "hashes": [ 69 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 70 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 71 | ], 72 | "index": "pypi", 73 | "version": "==3.0.8" 74 | }, 75 | "flask-jwt-extended": { 76 | "hashes": [ 77 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" 78 | ], 79 | "index": "pypi", 80 | "version": "==3.24.1" 81 | }, 82 | "flask-login": { 83 | "hashes": [ 84 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 85 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 86 | ], 87 | "index": "pypi", 88 | "version": "==0.5.0" 89 | }, 90 | "flask-migrate": { 91 | "hashes": [ 92 | "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", 93 | "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" 94 | ], 95 | "index": "pypi", 96 | "version": "==2.5.3" 97 | }, 98 | "flask-sqlalchemy": { 99 | "hashes": [ 100 | "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", 101 | "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" 102 | ], 103 | "index": "pypi", 104 | "version": "==2.4.4" 105 | }, 106 | "flask-wtf": { 107 | "hashes": [ 108 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", 109 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720" 110 | ], 111 | "index": "pypi", 112 | "version": "==0.14.3" 113 | }, 114 | "gunicorn": { 115 | "hashes": [ 116 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", 117 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" 118 | ], 119 | "index": "pypi", 120 | "version": "==20.0.4" 121 | }, 122 | "idna": { 123 | "hashes": [ 124 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", 125 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" 126 | ], 127 | "index": "pypi", 128 | "version": "==3.2" 129 | }, 130 | "itsdangerous": { 131 | "hashes": [ 132 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 133 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 134 | ], 135 | "index": "pypi", 136 | "version": "==1.1.0" 137 | }, 138 | "jinja2": { 139 | "hashes": [ 140 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 141 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 142 | ], 143 | "index": "pypi", 144 | "version": "==2.11.2" 145 | }, 146 | "mako": { 147 | "hashes": [ 148 | "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", 149 | "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" 150 | ], 151 | "index": "pypi", 152 | "version": "==1.1.3" 153 | }, 154 | "markupsafe": { 155 | "hashes": [ 156 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 157 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 158 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 159 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 160 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 161 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 162 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 163 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 164 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 165 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 166 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 167 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 168 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 169 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 170 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 171 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 172 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 173 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 174 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 175 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 176 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 177 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 178 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 179 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 180 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 181 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 182 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 183 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 184 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 185 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 186 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 187 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 188 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 189 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 190 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 191 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 192 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 193 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 194 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 195 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 196 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 197 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 198 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 199 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 200 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 201 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 202 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 203 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 204 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 205 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 206 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 207 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 208 | ], 209 | "index": "pypi", 210 | "version": "==1.1.1" 211 | }, 212 | "pyjwt": { 213 | "hashes": [ 214 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 215 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 216 | ], 217 | "index": "pypi", 218 | "version": "==1.7.1" 219 | }, 220 | "python-dateutil": { 221 | "hashes": [ 222 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 223 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 224 | ], 225 | "index": "pypi", 226 | "version": "==2.8.1" 227 | }, 228 | "python-dotenv": { 229 | "hashes": [ 230 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 231 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 232 | ], 233 | "index": "pypi", 234 | "version": "==0.14.0" 235 | }, 236 | "python-editor": { 237 | "hashes": [ 238 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 239 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 240 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 241 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 242 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 243 | ], 244 | "index": "pypi", 245 | "version": "==1.0.4" 246 | }, 247 | "six": { 248 | "hashes": [ 249 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 250 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 251 | ], 252 | "index": "pypi", 253 | "version": "==1.15.0" 254 | }, 255 | "sqlalchemy": { 256 | "hashes": [ 257 | "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb", 258 | "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804", 259 | "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6", 260 | "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0", 261 | "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe", 262 | "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de", 263 | "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36", 264 | "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e", 265 | "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66", 266 | "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6", 267 | "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc", 268 | "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d", 269 | "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce", 270 | "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea", 271 | "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f", 272 | "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365", 273 | "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea", 274 | "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23", 275 | "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338", 276 | "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1", 277 | "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b", 278 | "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e", 279 | "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba", 280 | "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02", 281 | "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12", 282 | "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86", 283 | "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d", 284 | "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7", 285 | "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0", 286 | "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac", 287 | "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc", 288 | "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37" 289 | ], 290 | "index": "pypi", 291 | "version": "==1.3.19" 292 | }, 293 | "text-unidecode": { 294 | "hashes": [ 295 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", 296 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" 297 | ], 298 | "index": "pypi", 299 | "version": "==1.3" 300 | }, 301 | "werkzeug": { 302 | "hashes": [ 303 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 304 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 305 | ], 306 | "index": "pypi", 307 | "version": "==1.0.1" 308 | }, 309 | "wtforms": { 310 | "hashes": [ 311 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", 312 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" 313 | ], 314 | "index": "pypi", 315 | "version": "==2.3.3" 316 | } 317 | }, 318 | "develop": { 319 | "astroid": { 320 | "hashes": [ 321 | "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892", 322 | "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9" 323 | ], 324 | "markers": "python_version ~= '3.6'", 325 | "version": "==2.6.2" 326 | }, 327 | "autopep8": { 328 | "hashes": [ 329 | "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0", 330 | "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9" 331 | ], 332 | "index": "pypi", 333 | "version": "==1.5.7" 334 | }, 335 | "isort": { 336 | "hashes": [ 337 | "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813", 338 | "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e" 339 | ], 340 | "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", 341 | "version": "==5.9.2" 342 | }, 343 | "lazy-object-proxy": { 344 | "hashes": [ 345 | "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", 346 | "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", 347 | "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", 348 | "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", 349 | "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", 350 | "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", 351 | "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", 352 | "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", 353 | "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", 354 | "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", 355 | "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", 356 | "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", 357 | "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", 358 | "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", 359 | "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", 360 | "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", 361 | "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", 362 | "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", 363 | "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", 364 | "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", 365 | "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", 366 | "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" 367 | ], 368 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 369 | "version": "==1.6.0" 370 | }, 371 | "mccabe": { 372 | "hashes": [ 373 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 374 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 375 | ], 376 | "version": "==0.6.1" 377 | }, 378 | "psycopg2-binary": { 379 | "hashes": [ 380 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 381 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 382 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 383 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 384 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 385 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 386 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 387 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 388 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 389 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 390 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 391 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 392 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 393 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 394 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 395 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 396 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 397 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 398 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 399 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 400 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 401 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 402 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 403 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 404 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 405 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 406 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 407 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 408 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 409 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 410 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 411 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 412 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 413 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 414 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 415 | ], 416 | "index": "pypi", 417 | "version": "==2.8.6" 418 | }, 419 | "pycodestyle": { 420 | "hashes": [ 421 | "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", 422 | "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" 423 | ], 424 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 425 | "version": "==2.7.0" 426 | }, 427 | "pylint": { 428 | "hashes": [ 429 | "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a", 430 | "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc" 431 | ], 432 | "index": "pypi", 433 | "version": "==2.9.3" 434 | }, 435 | "toml": { 436 | "hashes": [ 437 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 438 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 439 | ], 440 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 441 | "version": "==0.10.2" 442 | }, 443 | "wrapt": { 444 | "hashes": [ 445 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 446 | ], 447 | "version": "==1.12.1" 448 | } 449 | } 450 | } 451 | --------------------------------------------------------------------------------