├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── review.rb │ ├── booking.rb │ ├── spot.rb │ └── user.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── bnb.png │ │ ├── mainpage.png │ │ ├── searchpage.png │ │ ├── Logomakr_0gqPjJ.png │ │ └── Logomakr_6djnyC.png │ ├── stylesheets │ │ ├── api │ │ │ ├── reviews.scss │ │ │ ├── spots.scss │ │ │ ├── users.scss │ │ │ ├── bookings.scss │ │ │ └── sessions.scss │ │ ├── static_pages.scss │ │ ├── css_reset.scss │ │ ├── CalendarMonthGrid.scss │ │ ├── CalendarMonth.scss │ │ ├── variables.scss │ │ ├── DateRangePicker.scss │ │ ├── footer.scss │ │ ├── review.scss │ │ ├── DateRangePickerInput.scss │ │ ├── DateInput.scss │ │ ├── DayPicker.scss │ │ ├── DayPickerNavigation.scss │ │ ├── search.scss │ │ ├── booking.scss │ │ ├── modal.scss │ │ ├── CalendarDay.scss │ │ ├── spots.scss │ │ └── application.css.scss │ └── javascripts │ │ ├── api │ │ ├── spots.coffee │ │ ├── users.coffee │ │ ├── bookings.coffee │ │ ├── reviews.coffee │ │ └── sessions.coffee │ │ ├── static_pages.coffee │ │ └── application.js ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── users_controller.rb │ │ ├── sessions_controller.rb │ │ ├── reviews_controller.rb │ │ ├── spots_controller.rb │ │ └── bookings_controller.rb │ └── application_controller.rb ├── helpers │ ├── api │ │ ├── spots_helper.rb │ │ ├── users_helper.rb │ │ ├── bookings_helper.rb │ │ ├── reviews_helper.rb │ │ └── sessions_helper.rb │ ├── application_helper.rb │ └── static_pages_helper.rb └── views │ ├── api │ ├── users │ │ ├── _user.json.jbuilder │ │ └── show.json.jbuilder │ ├── sessions │ │ ├── _user.json.jbuilder │ │ └── show.json.jbuilder │ ├── reviews │ │ ├── show.json.jbuilder │ │ └── index.json.jbuilder │ ├── bookings │ │ ├── show.json.jbuilder │ │ └── index.json.jbuilder │ └── spots │ │ ├── show.json.jbuilder │ │ └── index.json.jbuilder │ ├── static_pages │ └── root.html.erb │ └── layouts │ └── application.html.erb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── review_test.rb │ ├── booking_test.rb │ ├── user_test.rb │ └── spot_test.rb ├── controllers │ ├── .keep │ ├── api │ │ ├── spots_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── bookings_controller_test.rb │ │ ├── reviews_controller_test.rb │ │ └── sessions_controller_test.rb │ └── static_pages_controller_test.rb ├── fixtures │ ├── .keep │ ├── reviews.yml │ ├── users.yml │ ├── bookings.yml │ └── spots.yml ├── integration │ └── .keep └── test_helper.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── docs ├── wireframes │ ├── Booking.png │ ├── SpotShow.png │ ├── SpotSearch.png │ ├── LogIn Modal.png │ ├── SIgnUp Modal.png │ ├── IndividualHome.png │ ├── FrontPage LoggedIn.png │ └── FrontPage LoggedOut.png ├── api-endpoints.md ├── component-hierarchy.md ├── sample-state.md ├── README.md └── schema.md ├── bin ├── bundle ├── rake ├── rails ├── spring └── setup ├── config ├── boot.rb ├── initializers │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── to_time_preserves_timezone.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── environment.rb ├── routes.rb ├── locales │ └── en.yml ├── secrets.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── database.yml ├── config.ru ├── db ├── migrate │ ├── 20170428142247_add_rules_to_spots.rb │ ├── 20170426132114_add_guest_limit_to_spots.rb │ ├── 20170428142732_add_null_false_to_rules.rb │ ├── 20170426132504_add_null_false_to_spot_guest_limit.rb │ ├── 20170418223725_add_names_to_users.rb │ ├── 20170418161431_create_users.rb │ ├── 20170427141328_create_reviews.rb │ ├── 20170426152955_create_bookings.rb │ └── 20170420182132_create_spots.rb ├── schema.rb └── seeds.rb ├── frontend ├── util │ ├── search_api_util.js │ ├── spot_api_util.js │ ├── booking_api_util.js │ ├── review_api_util.js │ ├── session_api_util.js │ └── marker_manager.js ├── components │ ├── homepage │ │ ├── homepage.jsx │ │ └── homepage_container.js │ ├── spots │ │ ├── spot_show_container.js │ │ ├── spot_index_container.js │ │ ├── spot_index.jsx │ │ ├── spot_index_item.jsx │ │ └── spot_show.jsx │ ├── greeting │ │ ├── greeting_container.js │ │ └── greeting.jsx │ ├── carousel │ │ ├── spot_carousel_container.js │ │ └── spot_carousel.jsx │ ├── review │ │ ├── review_form_container.js │ │ ├── review_index_container.js │ │ ├── review_index_item.jsx │ │ ├── review_index.jsx │ │ └── review_form.jsx │ ├── booking │ │ ├── booking_index_container.js │ │ ├── booking_container.js │ │ ├── booking_index_item.jsx │ │ ├── booking_index.jsx │ │ └── booking.jsx │ ├── app.jsx │ ├── modal │ │ ├── modal_container.js │ │ └── modal.jsx │ ├── navigation │ │ ├── navigation_bar_container.js │ │ ├── search_bar_container.js │ │ ├── navigation_bar.jsx │ │ └── search_bar.jsx │ ├── search │ │ ├── search_container.js │ │ └── search.jsx │ ├── login │ │ ├── login_form_container.js │ │ └── login_form.jsx │ ├── signup │ │ ├── signup_form_container.js │ │ └── signup_form.jsx │ ├── footer │ │ └── footer.jsx │ ├── root.jsx │ └── map │ │ └── map.jsx ├── actions │ ├── modal_actions.js │ ├── filter_actions.js │ ├── review_actions.js │ ├── spot_actions.js │ ├── booking_actions.js │ └── session_actions.js ├── reducers │ ├── spot_reducer.js │ ├── review_reducer.js │ ├── root_reducer.js │ ├── filters_reducer.js │ ├── modal_reducer.js │ ├── booking_reducer.js │ └── session_reducer.js ├── store │ └── store.js └── entry.jsx ├── Rakefile ├── .gitignore ├── webpack.config.js ├── package.json ├── Gemfile ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/api/spots_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SpotsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/bookings_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::BookingsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/reviews_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::ReviewsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :fname, :lname 2 | -------------------------------------------------------------------------------- /app/views/api/sessions/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :fname, :lname 2 | -------------------------------------------------------------------------------- /app/views/api/sessions/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! @user, :fname, :lname 2 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/users/user', user: @user 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/views/api/reviews/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @review, :id, :author, :spot, :rating, :body 2 | -------------------------------------------------------------------------------- /app/assets/images/bnb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/app/assets/images/bnb.png -------------------------------------------------------------------------------- /docs/wireframes/Booking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/Booking.png -------------------------------------------------------------------------------- /docs/wireframes/SpotShow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/SpotShow.png -------------------------------------------------------------------------------- /app/assets/images/mainpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/app/assets/images/mainpage.png -------------------------------------------------------------------------------- /docs/wireframes/SpotSearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/SpotSearch.png -------------------------------------------------------------------------------- /app/assets/images/searchpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/app/assets/images/searchpage.png -------------------------------------------------------------------------------- /docs/wireframes/LogIn Modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/LogIn Modal.png -------------------------------------------------------------------------------- /docs/wireframes/SIgnUp Modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/SIgnUp Modal.png -------------------------------------------------------------------------------- /app/views/api/bookings/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @booking, :id, :user, :spot, :start_date, :end_date, :guest_number 2 | -------------------------------------------------------------------------------- /docs/wireframes/IndividualHome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/IndividualHome.png -------------------------------------------------------------------------------- /app/assets/images/Logomakr_0gqPjJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/app/assets/images/Logomakr_0gqPjJ.png -------------------------------------------------------------------------------- /app/assets/images/Logomakr_6djnyC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/app/assets/images/Logomakr_6djnyC.png -------------------------------------------------------------------------------- /docs/wireframes/FrontPage LoggedIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/FrontPage LoggedIn.png -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /docs/wireframes/FrontPage LoggedOut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojibuh/BillionairBnB/HEAD/docs/wireframes/FrontPage LoggedOut.png -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/views/api/spots/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @spot, :id, :lat, :lng, :owner, :price, :location, :rules, :description 2 | json.image_url asset_path(@spot.image_url) 3 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /db/migrate/20170428142247_add_rules_to_spots.rb: -------------------------------------------------------------------------------- 1 | class AddRulesToSpots < ActiveRecord::Migration 2 | def change 3 | add_column :spots, :rules, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/reviews/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @reviews.each do |review| 2 | json.set! review.id do 3 | json.extract! review, :spot, :author, :rating, :body 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_BillionairBnB_session' 4 | -------------------------------------------------------------------------------- /app/views/api/bookings/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @bookings.each do |booking| 2 | json.set! booking.id do 3 | json.extract! booking, :spot, :start_date, :end_date, :guest_number 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170426132114_add_guest_limit_to_spots.rb: -------------------------------------------------------------------------------- 1 | class AddGuestLimitToSpots < ActiveRecord::Migration 2 | def change 3 | add_column :spots, :guest_limit, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170428142732_add_null_false_to_rules.rb: -------------------------------------------------------------------------------- 1 | class AddNullFalseToRules < ActiveRecord::Migration 2 | def change 3 | change_column :spots, :rules, :string, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/controllers/api/spots_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SpotsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/bookings_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::BookingsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/reviews_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::ReviewsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SessionsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/reviews.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/reviews controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/spots.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/spots controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170426132504_add_null_false_to_spot_guest_limit.rb: -------------------------------------------------------------------------------- 1 | class AddNullFalseToSpotGuestLimit < ActiveRecord::Migration 2 | def change 3 | change_column :spots, :guest_limit, :integer, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/bookings.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/bookings controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/sessions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/sessions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/spots/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @spots.each do |spot| 2 | json.set! spot.id do 3 | json.extract! spot, :id, :description, :lat, :lng, :location, :price 4 | json.image_url asset_path(spot.image_url) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /db/migrate/20170418223725_add_names_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNamesToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :fname, :string, null: false 4 | add_column :users, :lname, :string, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/spots.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/bookings.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/reviews.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/sessions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/static_pages.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /frontend/util/search_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchBounds = (address) => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: `https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=AIzaSyCeyaq4INBdNY82olkiTZT4o5RhTDTwLVs` 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../../config/application', __FILE__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /frontend/util/spot_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchSpots = (data) => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/spots', 5 | data: data 6 | }); 7 | }; 8 | 9 | export const fetchSpot = (id) => { 10 | return $.ajax({ 11 | method: 'GET', 12 | url: `/api/spots/${id}`, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/util/booking_api_util.js: -------------------------------------------------------------------------------- 1 | export const createBooking = (booking) => { 2 | return $.ajax({ 3 | method: 'POST', 4 | url: '/api/bookings', 5 | data: { booking } 6 | }); 7 | }; 8 | 9 | export const fetchBookings = () => { 10 | return $.ajax({ 11 | method: 'GET', 12 | url: '/api/bookings' 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/util/review_api_util.js: -------------------------------------------------------------------------------- 1 | export const createReview = (review) => { 2 | return $.ajax({ 3 | method: 'POST', 4 | url: '/api/reviews', 5 | data: { review } 6 | }); 7 | }; 8 | 9 | export const fetchReviews = (data) => { 10 | return $.ajax({ 11 | method: 'GET', 12 | url: '/api/reviews', 13 | data: data 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170418161431_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :email, null: false 5 | t.string :password_digest, null: false 6 | t.string :session_token, null: false 7 | 8 | t.timestamps null: false 9 | end 10 | 11 | add_index :users, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/css_reset.scss: -------------------------------------------------------------------------------- 1 | html, body, section, article, h1, h2, p, button, input, div, ul, form, li, figure, 2 | img { 3 | margin: 0; 4 | border: 0; 5 | padding: 0; 6 | font: inherit; 7 | text-align: inherit; 8 | vertical-align: inherit; 9 | text-decoration: inherit; 10 | color: inherit; 11 | background: transparent; 12 | outline: none; 13 | list-style: none; 14 | } 15 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | @user = User.new(user_params) 5 | if @user.save 6 | log_in(@user) 7 | render :show 8 | else 9 | render json: @user.errors , status: 422 10 | end 11 | end 12 | 13 | private 14 | def user_params 15 | params.require(:user).permit(:email, :fname, :lname, :password) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /db/migrate/20170427141328_create_reviews.rb: -------------------------------------------------------------------------------- 1 | class CreateReviews < ActiveRecord::Migration 2 | def change 3 | create_table :reviews do |t| 4 | t.integer :author_id, null: false 5 | t.integer :spot_id, null: false 6 | t.integer :rating, null: false 7 | t.string :body, null: false 8 | 9 | t.timestamps null: false 10 | end 11 | 12 | add_index :reviews, :author_id 13 | add_index :reviews, :spot_id 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | root to: 'static_pages#root' 4 | 5 | namespace :api, defaults: {format: :json} do 6 | resources :users, only: [:create] 7 | resource :session, only: [:create, :destroy] 8 | resources :spots, only: [:index, :create, :show, :destroy] 9 | resources :bookings, only: [:create, :index] 10 | resources :reviews, only: [:create, :index] 11 | end 12 | 13 | get '/*path', to: 'static_pages#root' 14 | end 15 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signup = (user) => { 2 | return $.ajax({ 3 | method: 'POST', 4 | url: '/api/users', 5 | data: { user } 6 | }); 7 | }; 8 | 9 | export const login = (user) => { 10 | return $.ajax({ 11 | method: 'POST', 12 | url: '/api/session', 13 | data: { user } 14 | }); 15 | }; 16 | 17 | export const logout = () => { 18 | return $.ajax({ 19 | method: 'DELETE', 20 | url: '/api/session' 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/components/homepage/homepage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GreetingContainer from '../greeting/greeting_container'; 3 | import SpotCarouselContainer from '../carousel/spot_carousel_container'; 4 | 5 | class Homepage extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default Homepage; 17 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /db/migrate/20170426152955_create_bookings.rb: -------------------------------------------------------------------------------- 1 | class CreateBookings < ActiveRecord::Migration 2 | def change 3 | create_table :bookings do |t| 4 | t.integer :user_id, null: false 5 | t.integer :spot_id, null: false 6 | t.date :start_date, null: false 7 | t.date :end_date, null: false 8 | t.integer :guest_number, null: false 9 | 10 | t.timestamps null: false 11 | end 12 | 13 | add_index :bookings, :user_id 14 | add_index :bookings, :spot_id 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /frontend/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_COMPONENT = "RECEIVE_COMPONENT"; 2 | export const ACTIVATE_MODAL = "ACTIVATE_MODAL"; 3 | export const DEACTIVATE_MODAL = "DEACTIVATE_MODAL"; 4 | 5 | export const receiveComponent = (modalType) => ({ 6 | type: RECEIVE_COMPONENT, 7 | modalType 8 | }); 9 | 10 | export const activateModal = (modalType) => ({ 11 | type: ACTIVATE_MODAL, 12 | modalType 13 | }); 14 | 15 | export const deactivateModal = (modalType) => ({ 16 | type: DEACTIVATE_MODAL, 17 | modalType 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/components/spots/spot_show_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchSpot } from '../../actions/spot_actions'; 3 | import SpotShow from './spot_show'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | spot: state.spots[ownProps.params.spotId] 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | fetchSpot: id => dispatch(fetchSpot(id)) 13 | }); 14 | 15 | export default connect( 16 | mapStateToProps, 17 | mapDispatchToProps 18 | )(SpotShow); 19 | -------------------------------------------------------------------------------- /frontend/reducers/spot_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_SPOTS, RECEIVE_SPOT } from '../actions/spot_actions'; 2 | import merge from 'lodash/merge'; 3 | const initialState = {}; 4 | 5 | export default (state = initialState, action) => { 6 | Object.freeze(state); 7 | switch(action.type) { 8 | case RECEIVE_SPOTS: 9 | return action.spots; 10 | case RECEIVE_SPOT: 11 | const newSpot = {[action.spot.id]: action.spot}; 12 | return merge({}, state, newSpot); 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Greeting from './greeting'; 3 | import { logout } from '../../actions/session_actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | currentUser: state.session.currentUser 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | logout: () => dispatch(logout()) 14 | }; 15 | }; 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(Greeting); 21 | -------------------------------------------------------------------------------- /frontend/components/homepage/homepage_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Homepage from './homepage'; 3 | import { logout } from '../../actions/session_actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | currentUser: state.session.currentUser 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | logout: () => dispatch(logout()) 14 | }; 15 | }; 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(Homepage); 21 | -------------------------------------------------------------------------------- /frontend/components/carousel/spot_carousel_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SpotCarousel from './spot_carousel'; 3 | import { fetchSpots } from '../../actions/spot_actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | spots: state.spots 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | fetchSpots: () => dispatch(fetchSpots()) 14 | }; 15 | }; 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(SpotCarousel); 21 | -------------------------------------------------------------------------------- /frontend/components/spots/spot_index_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SpotIndex from './spot_index'; 3 | import { fetchSpots } from '../../actions/spot_actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | spots: state.spots 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | fetchSpots: (filters) => dispatch(fetchSpots(filters)) 14 | }; 15 | }; 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(SpotIndex); 21 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | // import { createLogger } from 'redux-logger'; 4 | import RootReducer from '../reducers/root_reducer'; 5 | 6 | const middlewares = [thunk]; 7 | 8 | if (process.env.NODE_ENV !== 'production') { 9 | // middlewares.push(createLogger()); 10 | } 11 | 12 | const configureStore = (preloadedState = {}) => ( 13 | createStore(RootReducer, preloadedState, applyMiddleware(...middlewares)) 14 | ); 15 | 16 | export default configureStore; 17 | -------------------------------------------------------------------------------- /frontend/components/review/review_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ReviewForm from './review_form'; 3 | import { createReview } from '../../actions/review_actions'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | currentUser: state.session.currentUser 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = dispatch => { 12 | return { 13 | createReview: (review) => dispatch(createReview(review)) 14 | }; 15 | }; 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(ReviewForm); 21 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/to_time_preserves_timezone.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Preserve the timezone of the receiver when calling to `to_time`. 4 | # Ruby 2.4 will change the behavior of `to_time` to preserve the timezone 5 | # when converting to an instance of `Time` instead of the previous behavior 6 | # of converting to the local system timezone. 7 | # 8 | # Rails 5.0 introduced this config option so that apps made with earlier 9 | # versions of Rails are not affected when upgrading. 10 | ActiveSupport.to_time_preserves_timezone = true 11 | -------------------------------------------------------------------------------- /test/models/review_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: reviews 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # spot_id :integer not null 8 | # rating :integer not null 9 | # body :string not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | require 'test_helper' 15 | 16 | class ReviewTest < ActiveSupport::TestCase 17 | # test "the truth" do 18 | # assert true 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /frontend/reducers/review_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_REVIEWS, RECEIVE_REVIEW } from '../actions/review_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const initialState = {}; 5 | 6 | export default (state = initialState, action) => { 7 | Object.freeze(state); 8 | switch(action.type) { 9 | case RECEIVE_REVIEWS: 10 | return action.reviews; 11 | case RECEIVE_REVIEW: 12 | const newReview = { 13 | [action.review.id]: action.review 14 | }; 15 | return merge({}, state, newReview); 16 | default: 17 | return state; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | helper_method :current_user, :logged_in?, :log_in 7 | 8 | def current_user 9 | @current_user ||= User.find_by_session_token(session[:session_token]) 10 | end 11 | 12 | def logged_in? 13 | !!current_user 14 | end 15 | 16 | def log_in(user) 17 | session[:session_token] = user.session_token 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/components/review/review_index_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ReviewIndex from './review_index'; 3 | import { fetchReviews } from '../../actions/review_actions'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | reviews: state.reviews 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = dispatch => { 13 | return { 14 | fetchReviews: (id) => dispatch(fetchReviews(id)) 15 | }; 16 | }; 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(ReviewIndex); 22 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /db/migrate/20170420182132_create_spots.rb: -------------------------------------------------------------------------------- 1 | class CreateSpots < ActiveRecord::Migration 2 | def change 3 | create_table :spots do |t| 4 | t.float :lat, null: false 5 | t.float :lng, null: false 6 | t.integer :owner_id, null: false 7 | t.integer :price, null: false 8 | t.string :location, null: false 9 | t.string :image_url, null: false 10 | t.text :description, null: false 11 | 12 | t.timestamps null: false 13 | end 14 | 15 | add_index :spots, :owner_id 16 | add_index :spots, :price 17 | add_index :spots, :image_url 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/components/booking/booking_index_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import BookingIndex from './booking_index'; 3 | import { fetchBookings } from '../../actions/booking_actions'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | bookings: state.bookings 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = dispatch => { 13 | return { 14 | fetchBookings: () => dispatch(fetchBookings()) 15 | }; 16 | }; 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(BookingIndex); 22 | -------------------------------------------------------------------------------- /frontend/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './components/root'; 4 | import configureStore from './store/store'; 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | const root = document.getElementById('root'); 8 | let store; 9 | if (window.currentUser) { 10 | const preloadedState = { session: {currentUser: window.currentUser } }; 11 | store = configureStore(preloadedState); 12 | } else { 13 | store = configureStore(); 14 | } 15 | window.store = store; 16 | ReactDOM.render(, root); 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavigationBarContainer from './navigation/navigation_bar_container'; 3 | import ModalContainer from './modal/modal_container'; 4 | import Footer from './footer/footer'; 5 | 6 | class App extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | return( 14 |
15 | 16 | 17 |
{ this.props.children }
18 |
19 |
20 | ) 21 | } 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | def destroy 3 | if current_user 4 | current_user.reset_session_token! 5 | @session_token = nil 6 | end 7 | 8 | render json: {} 9 | end 10 | 11 | def create 12 | @user = User.find_by_credentials( 13 | params[:user][:email], 14 | params[:user][:password] 15 | ) 16 | if @user 17 | log_in(@user) 18 | render 'api/users/show' 19 | else 20 | render( 21 | json: ['Invalid Credentials'], 22 | status: 422 23 | ) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/api/reviews_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ReviewsController < ApplicationController 2 | 3 | def index 4 | @reviews = Review.find_reviews(spot_id) 5 | render :index 6 | end 7 | 8 | def create 9 | @review = Review.new(review_params) 10 | @review.author_id = current_user.id 11 | if @review.save 12 | render :show 13 | else 14 | render json: @review.errors.full_messages 15 | end 16 | end 17 | 18 | private 19 | def review_params 20 | params.require(:review).permit(:spot_id, :rating, :body) 21 | end 22 | 23 | def spot_id 24 | params[:spot_id].to_i 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | !/log/.keep 13 | /tmp 14 | 15 | node_modules/ 16 | bundle.js 17 | bundle.js.map 18 | .byebug_history 19 | .DS_Store 20 | npm-debug.log 21 | 22 | # Ignore application configuration 23 | /config/application.yml 24 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import SessionReducer from './session_reducer'; 3 | import ModalReducer from './modal_reducer'; 4 | import SpotReducer from './spot_reducer'; 5 | import FiltersReducer from './filters_reducer'; 6 | import BookingReducer from './booking_reducer'; 7 | import ReviewReducer from './review_reducer'; 8 | 9 | const RootReducer = combineReducers({ 10 | session: SessionReducer, 11 | modals: ModalReducer, 12 | spots: SpotReducer, 13 | filters: FiltersReducer, 14 | bookings: BookingReducer, 15 | reviews: ReviewReducer 16 | }); 17 | 18 | export default RootReducer; 19 | -------------------------------------------------------------------------------- /test/models/booking_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookings 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # spot_id :integer not null 8 | # start_date :date not null 9 | # end_date :date not null 10 | # guest_number :integer not null 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | 15 | require 'test_helper' 16 | 17 | class BookingTest < ActiveSupport::TestCase 18 | # test "the truth" do 19 | # assert true 20 | # end 21 | end 22 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # email :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # fname :string not null 12 | # lname :string not null 13 | # 14 | 15 | require 'test_helper' 16 | 17 | class UserTest < ActiveSupport::TestCase 18 | # test "the truth" do 19 | # assert true 20 | # end 21 | end 22 | -------------------------------------------------------------------------------- /frontend/components/booking/booking_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Booking from './booking'; 3 | import { createBooking, clearErrors } from '../../actions/booking_actions'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | errors: state.bookings.errors, 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = dispatch => { 13 | return { 14 | createBooking: booking => dispatch(createBooking(booking)), 15 | clear: () => dispatch(clearErrors()) 16 | }; 17 | }; 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Booking); 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/CalendarMonthGrid.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .CalendarMonthGrid { 4 | background: $react-dates-color-white; 5 | z-index: 0; 6 | text-align: left; 7 | } 8 | 9 | .CalendarMonthGrid--animating { 10 | -webkit-transition: -webkit-transform 0.2s ease-in-out; 11 | -moz-transition: -moz-transform 0.2s ease-in-out; 12 | transition: transform 0.2s ease-in-out; 13 | z-index: 1; 14 | } 15 | 16 | .CalendarMonthGrid--horizontal { 17 | position: absolute; 18 | left: 9px; 19 | } 20 | 21 | .CalendarMonthGrid--vertical { 22 | margin: 0 auto; 23 | } 24 | 25 | .CalendarMonthGrid--vertical-scrollable { 26 | margin: 0 auto; 27 | overflow-y: scroll; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/components/modal/modal_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Modal from './modal'; 3 | import { receiveComponent, deactivateModal } from '../../actions/modal_actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | isOpen: state.modals.isOpen, 8 | modalType: state.modals.modalType 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return { 14 | receiveComponent: (modalType) => dispatch(receiveComponent(modalType)), 15 | deactivateModal: (modalType) => dispatch(deactivateModal(modalType)) 16 | }; 17 | }; 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Modal); 23 | -------------------------------------------------------------------------------- /frontend/components/navigation/navigation_bar_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import NavigationBar from './navigation_bar'; 3 | import { activateModal } from '../../actions/modal_actions'; 4 | import { logout } from '../../actions/session_actions'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | currentUser: state.session.currentUser 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return { 14 | activateModal: (modalType) => dispatch(activateModal(modalType)), 15 | logout: () => dispatch(logout()) 16 | }; 17 | }; 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(NavigationBar); 23 | -------------------------------------------------------------------------------- /test/fixtures/reviews.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: reviews 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # spot_id :integer not null 8 | # rating :integer not null 9 | # body :string not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 15 | 16 | one: 17 | author_id: 1 18 | spot_id: 1 19 | rating: 1 20 | body: MyString 21 | 22 | two: 23 | author_id: 1 24 | spot_id: 1 25 | rating: 1 26 | body: MyString 27 | -------------------------------------------------------------------------------- /frontend/components/navigation/search_bar_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SearchBar from './search_bar'; 3 | import { logout } from '../../actions/session_actions'; 4 | import { updateFilter } from '../../actions/filter_actions'; 5 | import { moment } from 'moment'; 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | startDate: '', 10 | endDate: '' 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | logout: () => dispatch(logout()), 17 | updateFilter: (filters) => dispatch(updateFilter(filters)) 18 | }; 19 | }; 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(SearchBar); 25 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class Greeting extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |
14 |
15 |

Where next?

  16 |

Live the good life

17 |

with BillionairBnB

18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default Greeting; 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /frontend/actions/filter_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/spot_api_util'; 2 | import { fetchSpots } from './spot_actions'; 3 | 4 | export const UPDATE_FILTER = "UPDATE_FILTER"; 5 | export const CLEAR_FILTER = "CLEAR_FILTER"; 6 | 7 | export const updateFilter = (filters) => (dispatch, getState) => { 8 | filters.forEach((filterAttrs) => { 9 | let filter = filterAttrs[0]; 10 | let value = filterAttrs[1]; 11 | dispatch(changeFilter(filter, value)); 12 | }); 13 | return fetchSpots(getState().filters)(dispatch); 14 | }; 15 | 16 | export const changeFilter = (filter, value) => ({ 17 | type: UPDATE_FILTER, 18 | filter, 19 | value 20 | }); 21 | 22 | export const clearFilter = () => ({ 23 | type: CLEAR_FILTER 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/actions/review_actions.js: -------------------------------------------------------------------------------- 1 | import * as ReviewApiUtil from '../util/review_api_util'; 2 | 3 | export const RECEIVE_REVIEWS = 'RECEIVE_REVIEWS'; 4 | export const RECEIVE_REVIEW = 'RECEIVE_REVIEW'; 5 | 6 | export const receiveReviews = reviews => ({ 7 | type: RECEIVE_REVIEWS, 8 | reviews 9 | }); 10 | 11 | export const receiveReview = review => ({ 12 | type: RECEIVE_REVIEW, 13 | review 14 | }); 15 | 16 | export const fetchReviews = (id) => dispatch => { 17 | return ReviewApiUtil.fetchReviews(id) 18 | .then(reviews => dispatch(receiveReviews(reviews))); 19 | }; 20 | 21 | export const createReview = review => dispatch => { 22 | return ReviewApiUtil.createReview(review) 23 | .then(review => dispatch(receiveReview(review))); 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/actions/spot_actions.js: -------------------------------------------------------------------------------- 1 | import * as SpotApiUtil from '../util/spot_api_util'; 2 | 3 | export const RECEIVE_SPOTS = "RECEIVE_SPOTS"; 4 | export const RECEIVE_SPOT = "RECEIVE_SPOT"; 5 | 6 | export const receiveSpots = (spots) => { 7 | return { 8 | type: RECEIVE_SPOTS, 9 | spots 10 | }; 11 | }; 12 | 13 | export const receiveSpot = (spot) => { 14 | return { 15 | type: RECEIVE_SPOT, 16 | spot 17 | }; 18 | }; 19 | 20 | export const fetchSpots = (filters) => dispatch => { 21 | return SpotApiUtil.fetchSpots(filters) 22 | .then((spots) => dispatch(receiveSpots(spots))); 23 | }; 24 | 25 | export const fetchSpot = id => dispatch => ( 26 | SpotApiUtil.fetchSpot(id) 27 | .then(spot => dispatch(receiveSpot(spot))) 28 | ); 29 | -------------------------------------------------------------------------------- /frontend/reducers/filters_reducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_FILTER, CLEAR_FILTER } from '../actions/filter_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const _defaultFilter = Object.freeze({ 5 | bounds: { 6 | northeast: {}, 7 | southwest: {} 8 | }, 9 | address: '', 10 | guests: 0, 11 | startDate: '', 12 | endDate: '' 13 | }); 14 | 15 | export default function FiltersReducer(state = _defaultFilter, action) { 16 | Object.freeze(state); 17 | switch(action.type) { 18 | case UPDATE_FILTER: 19 | const newFilter = { 20 | [action.filter]: action.value 21 | }; 22 | return merge({}, state, newFilter); 23 | case CLEAR_FILTER: 24 | return _defaultFilter; 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/models/spot_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: spots 4 | # 5 | # id :integer not null, primary key 6 | # lat :float not null 7 | # lng :float not null 8 | # owner_id :integer not null 9 | # price :integer not null 10 | # location :string not null 11 | # image_url :string not null 12 | # description :text not null 13 | # created_at :datetime not null 14 | # updated_at :datetime not null 15 | # guest_limit :integer not null 16 | # 17 | 18 | require 'test_helper' 19 | 20 | class SpotTest < ActiveSupport::TestCase 21 | # test "the truth" do 22 | # assert true 23 | # end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/review.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: reviews 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # spot_id :integer not null 8 | # rating :integer not null 9 | # body :string not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | class Review < ActiveRecord::Base 15 | validates :spot, :author, :rating, :body, presence: true 16 | 17 | def self.find_reviews(spot_id) 18 | Review.where(spot_id: spot_id) 19 | end 20 | 21 | belongs_to :spot 22 | 23 | belongs_to :author, 24 | class_name: "User", 25 | foreign_key: :author_id, 26 | primary_key: :id 27 | end 28 | -------------------------------------------------------------------------------- /frontend/components/spots/spot_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slider from 'react-slick'; 3 | import SpotIndexItem from './spot_index_item'; 4 | 5 | class SpotIndex extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | const spots = Object.values(this.props.spots); 13 | let allSpots = spots.map((spot, idx) => { 14 | return
; 15 | }); 16 | if (spots.length > 0) { 17 | return ( 18 |
19 | { allSpots } 20 |
21 | ); 22 | } else { 23 | return ( 24 |
25 | ); 26 | } 27 | } 28 | } 29 | 30 | export default SpotIndex; 31 | -------------------------------------------------------------------------------- /frontend/components/review/review_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ReviewIndexItem extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const review = this.props.review; 11 | return ( 12 |
13 |
14 |
15 | { review.author.fname } 16 |
17 |
18 | Rating: { review.rating } out of 5 19 |
20 |
21 |
22 | { review.body } 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default ReviewIndexItem; 30 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # email :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # fname :string not null 12 | # lname :string not null 13 | # 14 | 15 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 16 | 17 | one: 18 | email: MyString 19 | password_digest: MyString 20 | session_token: MyString 21 | 22 | two: 23 | email: MyString 24 | password_digest: MyString 25 | session_token: MyString 26 | -------------------------------------------------------------------------------- /frontend/components/search/search_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Search from './search'; 3 | import { fetchSpots } from '../../actions/spot_actions'; 4 | import { updateFilter, clearFilter } from '../../actions/filter_actions'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | spots: state.spots, 9 | bounds: state.filters.bounds, 10 | filters: state.filters, 11 | address: state.filters.address 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | fetchSpots: () => dispatch(fetchSpots()), 18 | updateFilter: (filters) => dispatch(updateFilter(filters)), 19 | clearFilter: () => dispatch(clearFilter()) 20 | }; 21 | }; 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(Search); 27 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BillionairBnB 5 | <%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?key=#{ENV['GOOGLE_MAPS']}" %> 6 | 7 | 8 | 9 | 10 | <%= stylesheet_link_tag 'application', media: 'all' %> 11 | <%= javascript_include_tag 'application' %> 12 | <%= csrf_meta_tags %> 13 | 14 | 15 | <%= yield %> 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/fixtures/bookings.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookings 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # spot_id :integer not null 8 | # start_date :date not null 9 | # end_date :date not null 10 | # guest_number :integer not null 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | 15 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 16 | 17 | one: 18 | user_id: 1 19 | spot_id: 1 20 | start_date: 2017-04-26 21 | end_date: 2017-04-26 22 | guest_number: 1 23 | 24 | two: 25 | user_id: 1 26 | spot_id: 1 27 | start_date: 2017-04-26 28 | end_date: 2017-04-26 29 | guest_number: 1 30 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | ### Root 6 | + GET / - loads up application 7 | 8 | ## JSON API 9 | 10 | ### Users 11 | + POST /api/users - create new user 12 | + PATCH /api/users - update user 13 | + GET /api/users - fetch user details 14 | 15 | ### Session 16 | + POST /api/session - Log In 17 | + DELETE /api/session - Log Out 18 | 19 | ### Spots 20 | + GET /api/spots - Listings search 21 | + POST /api/spots - host can create a spot 22 | + GET /api/spots/:id - leads to show page 23 | + PATCH /api/spots 24 | + DELETE /api/spots 25 | 26 | ### Bookings 27 | + GET /api/bookings - fetch all bookings 28 | + POST /api/bookings - create a booking 29 | + GET /api/bookings/:id - fetch one bookings 30 | + DELETE /api/bookings - cancel booking 31 | 32 | ### Reviews 33 | + POST api/bookings/bookingsId/reviews/:id - create review 34 | -------------------------------------------------------------------------------- /frontend/components/search/search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SpotIndex from '../spots/spot_index'; 3 | import Map from '../map/map'; 4 | 5 | class Search extends React.Component { 6 | 7 | componentDidMount() { 8 | this.props.fetchSpots(this.props.filters); 9 | } 10 | 11 | componentWillUnmount() { 12 | this.props.clearFilter(); 13 | } 14 | 15 | render() { 16 | return( 17 |
18 |
19 | 23 |
24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | export default Search; 33 | -------------------------------------------------------------------------------- /frontend/components/review/review_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReviewIndexItem from './review_index_item'; 3 | 4 | class ReviewIndex extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount() { 11 | this.props.fetchReviews({ 12 | spot_id: this.props.spot.id 13 | }); 14 | } 15 | 16 | render() { 17 | const reviews = Object.values(this.props.reviews); 18 | if(reviews.length > 0) { 19 | let allReviews = reviews.map((review, idx) => { 20 | return
; 21 | }); 22 | return ( 23 |
24 | { allReviews } 25 |
26 | ); 27 | } else { 28 | return (
); 29 | } 30 | } 31 | } 32 | 33 | export default ReviewIndex; 34 | -------------------------------------------------------------------------------- /frontend/components/booking/booking_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class BookingIndexItem extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const booking = this.props.booking; 11 | const spot = booking.spot; 12 | if (spot) { 13 | return ( 14 |
15 |
16 | { spot.location } 17 |
18 |
19 | 20 |
21 |
22 | { booking.start_date } to { booking.end_date } 23 |
24 |
25 | ); 26 | } else { 27 | return (
); 28 | } 29 | } 30 | } 31 | 32 | export default BookingIndexItem; 33 | -------------------------------------------------------------------------------- /frontend/components/login/login_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import LoginForm from './login_form'; 3 | import { login, clearErrors } from '../../actions/session_actions'; 4 | import { deactivateModal, activateModal } from '../../actions/modal_actions'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | loggedIn: !!state.session.currentUser, 9 | errors: state.session.errors, 10 | formType: 'login' 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | processForm: (user) => dispatch(login(user)), 17 | deactivate: (modalType) => dispatch(deactivateModal(modalType)), 18 | activate: (modalType) => dispatch(activateModal(modalType)), 19 | clear: () => dispatch(clearErrors()) 20 | }; 21 | }; 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(LoginForm); 27 | -------------------------------------------------------------------------------- /frontend/components/signup/signup_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SignupForm from './signup_form'; 3 | import { signup, clearErrors } from '../../actions/session_actions'; 4 | import { deactivateModal, activateModal } from '../../actions/modal_actions'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | loggedIn: !!state.session.currentUser, 9 | errors: state.session.errors, 10 | formType: 'signup' 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | processForm: (user) => dispatch(signup(user)), 17 | deactivate: (modalType) => dispatch(deactivateModal(modalType)), 18 | activate: (modalType) => dispatch(activateModal(modalType)), 19 | clear: () => dispatch(clearErrors()) 20 | }; 21 | }; 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(SignupForm); 27 | -------------------------------------------------------------------------------- /frontend/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_COMPONENT, ACTIVATE_MODAL, DEACTIVATE_MODAL } from '../actions/modal_actions'; 2 | 3 | const initialState = { 4 | isOpen: false, 5 | modalType: '' 6 | }; 7 | 8 | export default function ModalReducer(state = initialState, action) { 9 | Object.freeze(state); 10 | let newState; 11 | switch(action.type){ 12 | case RECEIVE_COMPONENT: 13 | let modalType = action.modalType; 14 | return Object.assign({}, state, { modalType }); 15 | case ACTIVATE_MODAL: 16 | let activatedModalType = action.modalType; 17 | return Object.assign({}, state, {modalType: activatedModalType, isOpen: true}); 18 | case DEACTIVATE_MODAL: 19 | let deactivatedModalType = action.modalType; 20 | return Object.assign({}, state, {modalType: deactivatedModalType, isOpen: false}); 21 | default: 22 | return state; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/reducers/booking_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_BOOKINGS, RECEIVE_BOOKING, RECEIVE_ERRORS, CLEAR_ERRORS } from '../actions/booking_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const initialState = { 5 | errors: [] 6 | }; 7 | 8 | export default (state = initialState, action) => { 9 | Object.freeze(state); 10 | switch(action.type) { 11 | case RECEIVE_BOOKINGS: 12 | return action.bookings; 13 | case RECEIVE_BOOKING: 14 | const booking = {[action.booking.id]: action.booking}; 15 | return merge({}, state, booking); 16 | case RECEIVE_ERRORS: 17 | const errors = action.errors; 18 | let newState = merge({}, state); 19 | newState.errors = errors; 20 | return newState; 21 | case CLEAR_ERRORS: 22 | let noErrors = action.errors; 23 | return Object.assign({}, state, { errors: noErrors }); 24 | default: 25 | return state; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ERRORS, RECEIVE_CURRENT_USER, CLEAR_ERRORS } from '../actions/session_actions'; 2 | 3 | const initialState = { 4 | currentUser: null, 5 | errors: [] 6 | }; 7 | 8 | export default function SessionReducer(state = initialState, action) { 9 | Object.freeze(state); 10 | let newState; 11 | switch(action.type){ 12 | case RECEIVE_CURRENT_USER: 13 | newState = Object.assign({}, state); 14 | newState.currentUser = action.currentUser; 15 | newState.errors = []; 16 | return newState; 17 | case RECEIVE_ERRORS: 18 | const errors = action.errors; 19 | newState = Object.assign({}, state); 20 | newState.errors = errors; 21 | return newState; 22 | case CLEAR_ERRORS: 23 | let noErrors = action.errors; 24 | return Object.assign({}, state, { errors: noErrors }); 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/components/modal/modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoginFormContainer from '../login/login_form_container'; 3 | import SignupFormContainer from '../signup/signup_form_container'; 4 | 5 | class Modal extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | this.props.receiveComponent(); 13 | } 14 | 15 | render() { 16 | const form = this.props.modalType === 'login' ? : ; 17 | if (this.props.isOpen) { 18 | return ( 19 |
20 |
this.props.deactivateModal(this.props.modalType)}>
21 |
22 | { form } 23 |
24 |
25 | ); 26 | } else { 27 | return ( 28 |
29 | ); 30 | } 31 | } 32 | } 33 | 34 | export default Modal; 35 | -------------------------------------------------------------------------------- /test/fixtures/spots.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: spots 4 | # 5 | # id :integer not null, primary key 6 | # lat :float not null 7 | # lng :float not null 8 | # owner_id :integer not null 9 | # price :integer not null 10 | # location :string not null 11 | # image_url :string not null 12 | # description :text not null 13 | # created_at :datetime not null 14 | # updated_at :datetime not null 15 | # guest_limit :integer not null 16 | # 17 | 18 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 19 | 20 | one: 21 | lat: 1.5 22 | lng: 1.5 23 | owner_id: 1 24 | price: 1 25 | location: MyString 26 | image_url: MyString 27 | description: MyText 28 | 29 | two: 30 | lat: 1.5 31 | lng: 1.5 32 | owner_id: 1 33 | price: 1 34 | location: MyString 35 | image_url: MyString 36 | description: MyText 37 | -------------------------------------------------------------------------------- /frontend/components/spots/spot_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class SpotIndexItem extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const spot = this.props.spot; 12 | let description; 13 | if (spot.description.length >= 36) { 14 | description = spot.description.slice(0, 36) + "..."; 15 | } else { 16 | description = spot.description; 17 | } 18 | return( 19 | 20 |
21 |
22 | 23 |
24 |
25 | {`$${spot.price},`} 26 | {description} 27 |
28 |
29 | 30 | ); 31 | } 32 | } 33 | 34 | export default SpotIndexItem; 35 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | # Component Hierarchy 2 | 3 | ### HeaderContainer 4 | + HeaderLoggedIn 5 | + HeaderLoggedOut 6 | + LogInModal 7 | + LogInContainer 8 | + AuthForm 9 | 10 | + SignUpModal 11 | + SignUpContainer 12 | + AuthForm 13 | 14 | ### HomeContainer 15 | + SearchBarContainer 16 | + SearchBar 17 | + FeaturedHomesContainer 18 | + FeaturedHomes 19 | + HomeItem 20 | 21 | ### SearchContainer 22 | + MapContainer 23 | + HomesContainer 24 | + PriceRangeContainer 25 | + PriceRanges 26 | + SearchBarContainer 27 | + SearchBar 28 | 29 | ### SpotContainer 30 | + ReviewContainer 31 | + Review 32 | + SpotDetailContainer 33 | + SpotDetails 34 | + BookingsContainer 35 | + MapContainer 36 | 37 | ### BookingsContainer 38 | + CheckInContainer 39 | + CheckInDate 40 | + CheckOutContainer 41 | + CheckOutDate 42 | 43 | # Routes 44 | 45 | path | component 46 | --------------- |----------- 47 | '/' | "HeaderContainer" 48 | '/home' | "HomeContainer" 49 | '/spots/:spotId'| "SpotContainer" 50 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 9220a712540904d171ebcc127d0c385b912f1964a383b18b51debc7bcb4fa8e2bc3ee214a229bb1dcb855ce4e768cdcfbcbb0042a0b1af5743d926adc40b08d2 15 | 16 | test: 17 | secret_key_base: 7934044b4abb067c2105b2d46b600f13eb765a200ac398918c47da5c2ddb35d8ec8839c5022a3517851306647f91ac7bc63a39a1d3eb0eaef6f1b9320bb77559 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require("webpack"); 3 | 4 | var plugins = []; 5 | var devPlugins = []; 6 | 7 | var prodPlugins = [ 8 | new webpack.DefinePlugin({ 9 | 'process.env': { 10 | 'NODE_ENV': JSON.stringify('production') 11 | } 12 | }), 13 | new webpack.optimize.UglifyJsPlugin({ 14 | compress: { 15 | warnings: true 16 | } 17 | }) 18 | ]; 19 | 20 | plugins = plugins.concat( 21 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins 22 | ) 23 | 24 | module.exports = { 25 | entry: './frontend/entry.jsx', 26 | output: { 27 | filename: './app/assets/javascripts/bundle.js', 28 | }, 29 | plugins: plugins, 30 | module: { 31 | loaders: [ 32 | { 33 | test: [/\.jsx?$/], 34 | exclude: /(node_modules)/, 35 | loader: 'babel-loader', 36 | query: { 37 | presets: ['es2015', 'react'] 38 | } 39 | } 40 | ] 41 | }, 42 | devtool: 'source-map', 43 | resolve: { 44 | extensions: ['.js', '.jsx', '*'] 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /frontend/components/footer/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Footer extends React.Component { 4 | 5 | render() { 6 | return ( 7 |
8 |
9 |
10 |

This is a Moktar Jama production

11 | Check out my site! 12 |
13 |
14 |
    15 |
  • 16 |
  • 17 |
  • 18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | } 25 | 26 | export default Footer; 27 | -------------------------------------------------------------------------------- /frontend/components/booking/booking_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BookingIndexItem from './booking_index_item'; 3 | 4 | class BookingIndex extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount() { 11 | this.props.fetchBookings(); 12 | } 13 | 14 | render() { 15 | const bookings = Object.values(this.props.bookings); 16 | if(bookings.length > 0 && this.props.currentUser) { 17 | let allBookings = bookings.map((booking, idx) => { 18 | return
19 | 20 |
; 21 | }); 22 | return ( 23 |
24 |

Your Trips

25 |
26 | { allBookings } 27 |
28 |
29 | ); 30 | } else { 31 | return (
You have not made any bookings yet
); 32 | } 33 | } 34 | } 35 | 36 | export default BookingIndex; 37 | -------------------------------------------------------------------------------- /app/controllers/api/spots_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SpotsController < ApplicationController 2 | 3 | def index 4 | spots = bounds ? Spot.in_bounds(bounds) : Spot.all 5 | @spots = guests ? spots.can_fit_guests(guests.to_i) : spots 6 | render :index 7 | end 8 | 9 | def create 10 | @spot = Spot.new(spot_params) 11 | if @spot.save 12 | render :show 13 | else 14 | render json: @spot.errors.full_messages 15 | end 16 | end 17 | 18 | def show 19 | @spot = Spot.find_by(id: params[:id]) 20 | if @spot 21 | render :show 22 | else 23 | render json: @spot.errors.full_messages 24 | end 25 | end 26 | 27 | def destroy 28 | #save this for later 29 | end 30 | 31 | private 32 | def spot_params 33 | params.require(:spots).permit(:price, :location, :image_url, :description, :owner_id) 34 | end 35 | 36 | def bounds 37 | params[:bounds] 38 | end 39 | 40 | def start_date 41 | params["start_date"] 42 | end 43 | 44 | def end_date 45 | params["end_date"] 46 | end 47 | 48 | def guests 49 | params[:guests] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /frontend/actions/booking_actions.js: -------------------------------------------------------------------------------- 1 | import * as BookingApiUtil from '../util/booking_api_util'; 2 | 3 | export const RECEIVE_BOOKINGS = "RECEIVE_BOOKINGS"; 4 | export const RECEIVE_BOOKING = "RECEIVE_BOOKING"; 5 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS"; 6 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 7 | 8 | export const receiveBookings = bookings => ({ 9 | type: RECEIVE_BOOKINGS, 10 | bookings 11 | }); 12 | 13 | export const receiveBooking = booking => ({ 14 | type: RECEIVE_BOOKING, 15 | booking 16 | }); 17 | 18 | export const receiveErrors = errors => ({ 19 | type: RECEIVE_ERRORS, 20 | errors 21 | }); 22 | 23 | export const clearErrors = () => ({ 24 | type: CLEAR_ERRORS, 25 | errors: [] 26 | }); 27 | 28 | export const fetchBookings = () => dispatch => { 29 | return BookingApiUtil.fetchBookings() 30 | .then(bookings => dispatch(receiveBookings(bookings))); 31 | }; 32 | 33 | export const createBooking = booking => dispatch => { 34 | return BookingApiUtil.createBooking(booking) 35 | .then(booking => dispatch(receiveBooking(booking)), err => dispatch(receiveErrors(err.responseJSON))); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 4 | export const RECEIVE_ERRORS = 'RECEIVE_ERRORS'; 5 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 6 | 7 | export const login = user => dispatch => { 8 | return APIUtil.login(user) 9 | .then(user => dispatch(receiveCurrentUser(user)), err => dispatch(receiveErrors(err.responseJSON))); 10 | }; 11 | 12 | export const signup = user => dispatch => { 13 | return APIUtil.signup(user) 14 | .then(user => dispatch(receiveCurrentUser(user)), err => dispatch(receiveErrors(err.responseJSON))); 15 | }; 16 | 17 | export const logout = () => dispatch => { 18 | return APIUtil.logout() 19 | .then(() => dispatch(receiveCurrentUser(null))); 20 | }; 21 | 22 | export const receiveCurrentUser = currentUser => ({ 23 | type: RECEIVE_CURRENT_USER, 24 | currentUser 25 | }); 26 | 27 | export const receiveErrors = errors => ({ 28 | type: RECEIVE_ERRORS, 29 | errors 30 | }); 31 | 32 | export const clearErrors = () => ({ 33 | type: CLEAR_ERRORS, 34 | errors: [] 35 | }); 36 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module BillionairBnB 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | # Do not swallow errors in after_commit/after_rollback callbacks. 24 | config.active_record.raise_in_transactional_callbacks = true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/CalendarMonth.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "variables"; 3 | 4 | .CalendarMonth { 5 | text-align: center; 6 | padding: 0 13px; 7 | vertical-align: top; 8 | 9 | &:first-of-type { 10 | position: absolute; 11 | z-index: -1; 12 | opacity: 0; 13 | pointer-events: none; 14 | } 15 | 16 | table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | caption: { 20 | caption-side: initial; 21 | } 22 | } 23 | 24 | -moz-user-select: none; 25 | -webkit-user-select: none; 26 | -ms-user-select: none; 27 | user-select: none; 28 | } 29 | 30 | .CalendarMonth--horizontal { 31 | display: inline-block; 32 | min-height: 100%; 33 | } 34 | 35 | .CalendarMonth--vertical { 36 | display: block; 37 | } 38 | 39 | .CalendarMonth__caption { 40 | color: $react-dates-color-gray-dark; 41 | margin-top: 7px; 42 | font-size: 18px; 43 | text-align: center; 44 | // necessary to not hide borders in FF 45 | margin-bottom: 2px; 46 | caption-side: initial; 47 | } 48 | 49 | .CalendarMonth--horizontal .CalendarMonth__caption, 50 | .CalendarMonth--vertical .CalendarMonth__caption { 51 | padding: 15px 0 35px; 52 | } 53 | 54 | .CalendarMonth--vertical-scrollable .CalendarMonth__caption { 55 | padding: 5px 0; 56 | } 57 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'; 4 | import SearchContainer from './search/search_container'; 5 | import HomepageContainer from './homepage/homepage_container'; 6 | import SpotShowContainer from './spots/spot_show_container'; 7 | import BookingIndexContainer from './booking/booking_index_container'; 8 | import App from './app'; 9 | 10 | const Root = ({ store }) => { 11 | 12 | const redirectIfLoggedOut = (nextState, replace) => { 13 | if (!store.getState().session.currentUser) { 14 | replace('/'); 15 | } 16 | }; 17 | 18 | return ( 19 | 20 | window.scrollTo(0, 0)} history={ browserHistory } > 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Root; 33 | -------------------------------------------------------------------------------- /app/models/booking.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookings 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # spot_id :integer not null 8 | # start_date :date not null 9 | # end_date :date not null 10 | # guest_number :integer not null 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | 15 | class Booking < ActiveRecord::Base 16 | validates :user, :spot, :start_date, :end_date, :guest_number, presence: true 17 | 18 | def self.no_overlap?(book_start_date, book_end_date, spot_id) 19 | spot_bookings = Spot.find_by(id: spot_id).bookings 20 | spot_bookings.none? do |booking| 21 | !((booking.start_date > book_end_date) || (book_start_date > booking.end_date)) 22 | end 23 | end 24 | 25 | def self.date_convert(date) 26 | date_numbers = date.split(',').map(&:to_i) 27 | 28 | return Date.new(date_numbers[0], date_numbers[1], date_numbers[2]) 29 | end 30 | 31 | def self.guest_limit(spot_id) 32 | spot = Spot.find_by(id: spot_id) 33 | spot.guest_limit 34 | end 35 | 36 | def self.find_bookings(user_id) 37 | Booking.where(user_id: user_id) 38 | end 39 | 40 | belongs_to :user 41 | 42 | belongs_to :spot 43 | end 44 | -------------------------------------------------------------------------------- /frontend/util/marker_manager.js: -------------------------------------------------------------------------------- 1 | export default class MarkerManager { 2 | constructor(map, handleClick) { 3 | this.map = map; 4 | this.markers = {}; 5 | this.handleClick = handleClick; 6 | } 7 | 8 | updateMarkers(spots) { 9 | const spotsObj = {}; 10 | let spotList = Object.values(spots); 11 | 12 | if (spotList.length > 0) { 13 | spotList.forEach((spot) => { 14 | spotsObj[spot.id] = spot; 15 | }); 16 | 17 | spotList = spotList.filter(spot => !this.markers[spot.id]); 18 | spotList.forEach((spot) => { 19 | this.createMarkerFromSpot(spot, this.handleClick); 20 | }); 21 | 22 | } 23 | 24 | Object.keys(this.markers) 25 | .filter(spotId => !spotsObj[spotId]) 26 | .forEach((spotId) => this.removeMarker(this.markers[spotId])); 27 | } 28 | 29 | createMarkerFromSpot(spot) { 30 | let marker = new google.maps.Marker({ 31 | position: {lat: spot.lat, lng: spot.lng}, 32 | map: this.map, 33 | spotId: spot.id 34 | }); 35 | this.markers[marker.spotId] = marker; 36 | 37 | marker.addListener('click', () => this.handleClick(spot)); 38 | this.markers[marker.spotId] = marker; 39 | 40 | marker.setMap(this.map); 41 | } 42 | 43 | removeMarker(marker) { 44 | this.markers[marker.spotId].setMap(null); 45 | delete this.markers[marker.spotId]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/variables.scss: -------------------------------------------------------------------------------- 1 | $react-dates-width-input: 100px !default; 2 | $react-dates-width-arrow: 24px !default; 3 | $react-dates-width-tooltip-arrow: 20px !default; 4 | $react-dates-spacing-vertical-picker: 72px !default; 5 | 6 | $react-dates-color-primary: #00a699 !default; 7 | $react-dates-color-primary-dark: #00514a !default; 8 | $react-date-color-primary-dark-1: #008489 !default; 9 | $react-dates-color-primary-shade-1: #33dacd !default; 10 | $react-dates-color-primary-shade-2: #66e2da !default; 11 | $react-dates-color-primary-shade-3: #80e8e0 !default; 12 | $react-dates-color-primary-shade-4: #b2f1ec !default; 13 | $react-dates-color-secondary: #007a87 !default; 14 | $react-dates-color-white: #fff !default; 15 | $react-dates-color-gray: #565a5c !default; 16 | $react-dates-color-gray-dark: darken($react-dates-color-gray, 10.5%) !default; 17 | $react-dates-color-gray-light: lighten($react-dates-color-gray, 17.8%) !default; // #82888a 18 | $react-dates-color-gray-lighter: lighten($react-dates-color-gray, 45%) !default; // #cacccd 19 | $react-dates-color-gray-lightest: lighten($react-dates-color-gray, 60%) !default; 20 | $react-dates-color-highlighted: #ffe8bc !default; 21 | 22 | $react-dates-color-border: #dbdbdb !default; 23 | $react-dates-color-border-light: #dce0e0 !default; 24 | $react-dates-color-border-medium: #c4c4c4 !default; 25 | $react-dates-color-placeholder-text: #757575 !default; 26 | $react-dates-color-text: #676767 !default; 27 | $react-dates-color-text-focus: #007a87 !default; 28 | $react-dates-color-focus: #99ede6 !default; 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/DateRangePicker.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .screen-reader-only { 4 | border: 0; 5 | clip: rect(0, 0, 0, 0); 6 | height: 1px; 7 | margin: -1px; 8 | overflow: hidden; 9 | padding: 0; 10 | position: absolute; 11 | width: 1px; 12 | } 13 | 14 | .DateRangePicker { 15 | position: relative; 16 | display: inline-block; 17 | } 18 | 19 | .DateRangePicker__picker { 20 | z-index: 1; 21 | background-color: $react-dates-color-white; 22 | position: absolute; 23 | top: $react-dates-spacing-vertical-picker; 24 | } 25 | 26 | .DateRangePicker__picker--direction-left { 27 | left: 0; 28 | } 29 | 30 | .DateRangePicker__picker--direction-right { 31 | right: 0; 32 | } 33 | 34 | .DateRangePicker__picker--portal { 35 | background-color: rgba(0, 0, 0, 0.3); 36 | position: fixed; 37 | top: 0; 38 | left: 0; 39 | height: 100%; 40 | width: 100%; 41 | } 42 | 43 | .DateRangePicker__picker--full-screen-portal { 44 | background-color: $react-dates-color-white; 45 | } 46 | 47 | .DateRangePicker__close { 48 | background: none; 49 | border: 0; 50 | color: inherit; 51 | font: inherit; 52 | line-height: normal; 53 | overflow: visible; 54 | padding: 0; 55 | cursor: pointer; 56 | 57 | position: absolute; 58 | top: 0; 59 | right: 0; 60 | padding: 15px; 61 | z-index: 2; 62 | 63 | svg { 64 | height: 15px; 65 | width: 15px; 66 | fill: $react-dates-color-gray-lighter; 67 | } 68 | 69 | &:hover, 70 | &:focus { 71 | color: darken(#cacccd, 10%); 72 | text-decoration: none; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BillionairBnB", 3 | "version": "1.0.0", 4 | "description": "== README", 5 | "main": "webpack.config.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "engines": { 11 | "node": "6.7.0", 12 | "npm": "3.10.7" 13 | }, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "postinstall": "webpack" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/pojibuh/BillionairBnB.git" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/pojibuh/BillionairBnB/issues" 27 | }, 28 | "homepage": "https://github.com/pojibuh/BillionairBnB#readme", 29 | "dependencies": { 30 | "babel-core": "^6.24.1", 31 | "babel-loader": "^6.4.1", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-react": "^6.24.1", 34 | "lodash": "^4.17.4", 35 | "moment": "^2.15.1", 36 | "moment-range": "^3.0.3", 37 | "moment-timezone": "^0.5.13", 38 | "react": "^15.5.4", 39 | "react-addons-shallow-compare": "^15.5.2", 40 | "react-dates": "^10.1.1", 41 | "react-dom": "^15.5.4", 42 | "react-moment": "^0.2.2", 43 | "react-redux": "^5.0.4", 44 | "react-router": "^3.0.5", 45 | "react-slick": "^0.14.11", 46 | "redux": "^3.6.0", 47 | "redux-thunk": "^2.2.0", 48 | "slick-carousel": "^1.6.0", 49 | "webpack": "^2.4.1" 50 | }, 51 | "devDependencies": { 52 | "redux-logger": "^3.0.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ```js 2 | { 3 | users: { 4 | 1: id: 20, 5 | email: 'ashketchum@pokemon.com', 6 | fname: 'Ash', 7 | lname: 'Ketchum' 8 | } 9 | 1: id: 20, 10 | email: 'moktarjama@gmail.com', 11 | fname: 'Moktar', 12 | lname: 'Jama' 13 | } 14 | 1: id: 20, 15 | email: 'ashketchum@pokemon.com', 16 | fname: 'Derek', 17 | lname: 'Jeter' 18 | } 19 | 1: id: 20, 20 | email: 'ashketchum@pokemon.com', 21 | fname: 'Kobe', 22 | lname: 'Bryant' 23 | } 24 | 1: id: 20, 25 | email: 'ashketchum@pokemon.com', 26 | fname: 'Peyton', 27 | lname: 'Manning' 28 | } 29 | } 30 | session: { 31 | currentUser: { 32 | id: 20, 33 | email: 'ashketchum@pokemon.com', 34 | fname: 'Ash', 35 | lname: 'Ketchum' 36 | }, 37 | errors: [] 38 | }, 39 | spots: { 40 | 1: { 41 | id: 1, 42 | price: 5000, 43 | image_url: 'penthouse.jpg', 44 | description: 'ballin', 45 | location: 'Hong Kong' 46 | //etc... 47 | }, 48 | errors: [] 49 | }, 50 | bookings: { 51 | 1: { 52 | id: 1, 53 | place_name: 'Mega Mansion', 54 | start_date: 04/16/2017, 55 | end_date: 05/19/2017, 56 | guest_number: 3 57 | }, 58 | errors: [] 59 | }, 60 | reviews: { 61 | 1: { 62 | id: 1, 63 | spot_id: 1000, 64 | rating: 2, 65 | review_text: 'meh' 66 | }, 67 | errors: [] 68 | }, 69 | modals: { 70 | signupModal 71 | loginModal 72 | } 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /app/models/spot.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: spots 4 | # 5 | # id :integer not null, primary key 6 | # lat :float not null 7 | # lng :float not null 8 | # owner_id :integer not null 9 | # price :integer not null 10 | # location :string not null 11 | # image_url :string not null 12 | # description :text not null 13 | # created_at :datetime not null 14 | # updated_at :datetime not null 15 | # guest_limit :integer not null 16 | # 17 | 18 | class Spot < ActiveRecord::Base 19 | validates :lat, :lng, :owner, :price, :location, :image_url, :description, presence: true 20 | 21 | 22 | def self.in_bounds(bounds) 23 | self.where("lat < ?", bounds[:northeast][:lat]) 24 | .where("lat > ?", bounds[:southwest][:lat]) 25 | .where("lng > ?", bounds[:southwest][:lng]) 26 | .where("lng < ?", bounds[:northeast][:lng]) 27 | end 28 | 29 | def self.can_fit_guests(guests) 30 | self.where("guest_limit >= ?", guests) 31 | end 32 | 33 | # def available_spots(start_date, end_date) 34 | # available = Booking.where(spot_id: self.id) 35 | # .where(<<-SQL, start_date: start_date, end_date: end_date) 36 | # NOT( (start_date > :end_date) OR (end_date < :start_date) ) 37 | # SQL 38 | # return available 39 | # end 40 | 41 | has_many :bookings 42 | 43 | has_many :reviews 44 | 45 | belongs_to :owner, 46 | class_name: "User", 47 | foreign_key: :owner_id, 48 | primary_key: :id 49 | end 50 | -------------------------------------------------------------------------------- /app/assets/stylesheets/footer.scss: -------------------------------------------------------------------------------- 1 | .outer-footer { 2 | width: 100%; 3 | height: 120px; 4 | border-top: 1px solid #dce0e0; 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .inner-footer { 12 | width: 1070px; 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | align-items: center; 17 | } 18 | 19 | .inner-footer p { 20 | color: #767676; 21 | } 22 | 23 | .icons { 24 | cursor: default; 25 | display: flex; 26 | flex-direction: row; 27 | justify-content: space-around; 28 | } 29 | 30 | .icons a { 31 | cursor: pointer; 32 | } 33 | 34 | ul.icons li { 35 | display: inline-block; 36 | line-height: 1em; 37 | padding-left: 0.25em; 38 | cursor: pointer; 39 | } 40 | 41 | .icon { 42 | text-decoration: none; 43 | position: relative; 44 | font-family: FontAwesome; 45 | font-style: normal; 46 | font-size: 30px; 47 | font-weight: 500; 48 | } 49 | 50 | .icon.circle { 51 | transition: all 0.2s ease-in-out; 52 | border-radius: 100%; 53 | display: inline-block; 54 | height: 2em; 55 | left: 0; 56 | line-height: 2em; 57 | text-align: center; 58 | text-decoration: none; 59 | top: 0; 60 | width: 2em; 61 | } 62 | 63 | .icon.circle:hover { 64 | top: -0.2em; 65 | background-color: rgba(255, 255, 255, 0.075); 66 | } 67 | 68 | .fa-github { 69 | background: transparent; 70 | color: #676767; 71 | border: 1px solid #676767; 72 | } 73 | 74 | .fa-linkedin-square { 75 | background: transparent; 76 | color: #676767; 77 | border: 1px solid #676767; 78 | } 79 | 80 | .fa-angellist { 81 | background: transparent; 82 | color: #676767; 83 | border: 1px solid #676767; 84 | } 85 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # email :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # fname :string not null 12 | # lname :string not null 13 | # 14 | 15 | class User < ActiveRecord::Base 16 | validates :email, :password_digest, :session_token, presence: true 17 | validates :fname, :lname, presence: true 18 | validates :password, length: { minimum: 8, allow_nil: true } 19 | validates_email_format_of :email 20 | after_initialize :ensure_session_token 21 | 22 | attr_reader :password 23 | 24 | has_many :spots 25 | 26 | has_many :bookings 27 | 28 | has_many :reviews 29 | 30 | def self.find_by_credentials(email, password) 31 | user = User.find_by_email(email) 32 | user && user.is_password?(password) ? user : nil 33 | end 34 | 35 | def self.generate_session_token 36 | SecureRandom.urlsafe_base64(16) 37 | end 38 | 39 | def password=(password) 40 | @password = password 41 | self.password_digest = BCrypt::Password.create(password) 42 | end 43 | 44 | def is_password?(password) 45 | BCrypt::Password.new(self.password_digest).is_password?(password) 46 | end 47 | 48 | def reset_session_token! 49 | self.session_token = User.generate_session_token 50 | self.save! 51 | self.session_token 52 | end 53 | 54 | private 55 | def ensure_session_token 56 | self.session_token ||= User.generate_session_token 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '4.2.8' 6 | # Use postgresql as the database for Active Record 7 | gem 'pg', '~> 0.15' 8 | # Use SCSS for stylesheets 9 | gem 'sass-rails', '~> 5.0' 10 | # Use Uglifier as compressor for JavaScript assets 11 | gem 'uglifier', '>= 1.3.0' 12 | # Use CoffeeScript for .coffee assets and views 13 | gem 'coffee-rails', '~> 4.1.0' 14 | # See https://github.com/rails/execjs#readme for more supported runtimes 15 | # gem 'therubyracer', platforms: :ruby 16 | 17 | # Use jquery as the JavaScript library 18 | gem 'jquery-rails' 19 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 20 | gem 'jbuilder', '~> 2.0' 21 | # bundle exec rake doc:rails generates the API under doc/api. 22 | gem 'sdoc', '~> 0.4.0', group: :doc 23 | 24 | # Use ActiveModel has_secure_password 25 | gem 'bcrypt', '~> 3.1.7' 26 | 27 | gem 'rails_12factor' 28 | 29 | gem 'validates_email_format_of' 30 | 31 | gem 'figaro' 32 | 33 | gem 'faker' 34 | 35 | # Use Unicorn as the app server 36 | # gem 'unicorn' 37 | 38 | # Use Capistrano for deployment 39 | # gem 'capistrano-rails', group: :development 40 | 41 | group :development, :test do 42 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 43 | gem 'byebug' 44 | gem 'better_errors' 45 | gem 'binding_of_caller' 46 | gem 'pry-rails' 47 | gem 'annotate' 48 | end 49 | 50 | group :development do 51 | # Access an IRB console on exception pages or by using <%= console %> in views 52 | gem 'web-console', '~> 2.0' 53 | 54 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 55 | gem 'spring' 56 | end 57 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /app/assets/stylesheets/review.scss: -------------------------------------------------------------------------------- 1 | .spot-reviews { 2 | border-top: 1px solid #C0C0C0; 3 | width: 100%; 4 | } 5 | 6 | .individual-review { 7 | border-bottom: 1px solid #DCDCDC; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-around; 11 | align-items: space-around; 12 | padding: 10px 0 10px 0; 13 | } 14 | 15 | .review-top-half { 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: space-between; 19 | align-items: center; 20 | font-size: 20px; 21 | font-weight: 400; 22 | padding-bottom: 10px; 23 | } 24 | 25 | .review-bottom-half { 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: space-between; 29 | align-items: center; 30 | font-size: 16px; 31 | font-weight: 300; 32 | } 33 | 34 | .form-rating { 35 | font-size: 12px; 36 | text-align: center; 37 | border: 1px solid #DCDCDC; 38 | width: 110px; 39 | } 40 | 41 | .review-text { 42 | border: 1px solid #DCDCDC; 43 | } 44 | 45 | .review-text:focus { 46 | outline: none; 47 | } 48 | 49 | .rating { 50 | width: 100%; 51 | font-weight: 400; 52 | } 53 | 54 | .review-box { 55 | width: 300px; 56 | height: 170px; 57 | background-color: #DCDCDC; 58 | display: flex; 59 | flex-direction: row; 60 | justify-content: center; 61 | align-items: center; 62 | } 63 | 64 | .review-form { 65 | height: 250px; 66 | display: flex; 67 | flex-direction: column; 68 | justify-content: space-around; 69 | } 70 | 71 | .review-form-title { 72 | margin: 0 auto; 73 | padding: 10px 0 10px 0; 74 | } 75 | 76 | .review-form-submit { 77 | background-color: #FF5A5F; 78 | padding: 5px 20px 5px 20px; 79 | color: #fff; 80 | font-size: 16px; 81 | font-weight: 400; 82 | border-radius: 4px; 83 | width: 145px; 84 | } 85 | 86 | .review-form-submit:hover { 87 | cursor: pointer; 88 | } 89 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # BillionairBnB 2 | 3 | Heroku Link: http://billionairbnb.herokuapp.com/ 4 | Trello Link: (put link here) 5 | 6 | ## Minimum Viable Product 7 | 8 | BillionairBnB is a web application inspired by airBnB with the intention of providing a quality luxury experience to users. By the end of week 9, this app will satisfy, at minimum, the following critera: 9 | 10 | - [ ] Hosting on Heroku 11 | - [ ] Creation of user and guest/demo login 12 | - [ ] Spots 13 | - [ ] Bookings 14 | - [ ] Reviews 15 | - [ ] Spot search based on location and availability using Google Maps API 16 | - [ ] Production README 17 | 18 | ## Design Docs 19 | 20 | - [View Wireframes](wireframes/) 21 | - [React Components](component-hierarchy.md) 22 | - [API Endpoints](api-endpoints.md) 23 | - [DB Schema](schema.md) 24 | - [Sample State](sample-state.md) 25 | 26 | ## Implementation Timeline 27 | 28 | ### Phase 1: Setup User Auth for frontend and backend (2 days) 29 | Goal: Functional frontpage with login and signup modals 30 | 31 | ### Phase 2: Spot models and featured spots (2 days) 32 | Goal: Featured spots will be on the frontpage with links to spot show pages 33 | 34 | ### Phase 3: Spot search page with search bar and Google Maps integration (2 days) 35 | Goal: Fully functional search page with Google Maps API and toggle options 36 | 37 | ### Phase 4: Spot show pages (2 days) 38 | Goal: Create spot pages that show details as well as reviews 39 | 40 | ### Phase 5: Bookings model (1 day) 41 | Goal: Users can book spots, and the spot will be tied to user for duration of stay 42 | 43 | ### Phase 6: Review model (1 day) 44 | Goal: Users will be able to leave reviews on a spot show page 45 | 46 | ### Bonus Features 47 | 48 | - [ ] User/Host profiles and trips page 49 | - [ ] Messaging between user and host 50 | - [ ] Experiences on frontpage/experiences show page 51 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /frontend/components/spots/spot_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import BookingContainer from '../booking/booking_container'; 4 | import ReviewIndexContainer from '../review/review_index_container'; 5 | import ReviewFormContainer from '../review/review_form_container'; 6 | 7 | class SpotShow extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | componentDidMount() { 14 | this.props.fetchSpot(this.props.params.spotId); 15 | } 16 | 17 | render() { 18 | const spot = this.props.spot; 19 | if (spot && spot.owner) { 20 | const owner = spot.owner; 21 | return ( 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 | { spot.description } 30 |

{ spot.location }

31 |

{ owner.fname } { owner.lname }

32 |
33 |
34 |

House Rules

35 |

{ spot.rules }

36 |
37 |
38 | 39 | 40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | ); 48 | } else { 49 | return( 50 |
51 | ); 52 | } 53 | } 54 | } 55 | 56 | export default SpotShow; 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/DateRangePickerInput.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .DateRangePickerInput { 4 | background-color: $react-dates-color-white; 5 | border: 1px solid $react-dates-color-gray-lighter; 6 | display: inline-block; 7 | } 8 | 9 | .DateRangePickerInput--disabled { 10 | background: $react-dates-color-gray-lighter; 11 | } 12 | 13 | .DateRangePickerInput__arrow { 14 | display: inline-block; 15 | vertical-align: middle; 16 | } 17 | 18 | .DateRangePickerInput__arrow svg { 19 | vertical-align: middle; 20 | fill: $react-dates-color-text; 21 | height: $react-dates-width-arrow; 22 | width: $react-dates-width-arrow; 23 | } 24 | 25 | .DateRangePickerInput__clear-dates { 26 | background: none; 27 | border: 0; 28 | color: inherit; 29 | font: inherit; 30 | line-height: normal; 31 | overflow: visible; 32 | 33 | cursor: pointer; 34 | display: inline-block; 35 | vertical-align: middle; 36 | padding: 10px; 37 | margin: 0 10px 0 5px; 38 | } 39 | 40 | .DateRangePickerInput__clear-dates svg { 41 | fill: $react-dates-color-gray-light; 42 | height: 12px; 43 | width: 15px; 44 | vertical-align: middle; 45 | } 46 | 47 | .DateRangePickerInput__clear-dates--hide { 48 | visibility: hidden; 49 | } 50 | 51 | .DateRangePickerInput__clear-dates:focus, 52 | .DateRangePickerInput__clear-dates--hover { 53 | background: $react-dates-color-border; 54 | border-radius: 50%; 55 | } 56 | 57 | .DateRangePickerInput__calendar-icon { 58 | background: none; 59 | border: 0; 60 | color: inherit; 61 | font: inherit; 62 | line-height: normal; 63 | overflow: visible; 64 | 65 | cursor: pointer; 66 | display: inline-block; 67 | vertical-align: middle; 68 | padding: 10px; 69 | margin: 0 5px 0 10px; 70 | 71 | svg { 72 | fill: $react-dates-color-gray-light; 73 | height: 15px; 74 | width: 14px; 75 | vertical-align: middle; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /app/assets/stylesheets/DateInput.scss: -------------------------------------------------------------------------------- 1 | $caret-top: $react-dates-spacing-vertical-picker - $react-dates-width-tooltip-arrow / 2; 2 | 3 | .DateInput { 4 | // font 5 | font-weight: 400; 6 | font-size: 16px; 7 | line-height: 24px; 8 | color: $react-dates-color-placeholder-text; 9 | margin: 0; 10 | padding: 8px; 11 | 12 | background: $react-dates-color-white; 13 | position: relative; 14 | display: inline-block; 15 | width: $react-dates-width-input; 16 | vertical-align: middle; 17 | } 18 | 19 | #endDate { 20 | padding-right: 10px; 21 | } 22 | 23 | .DateInput--with-caret::before, 24 | .DateInput--with-caret::after { 25 | content: ""; 26 | display: inline-block; 27 | position: absolute; 28 | bottom: auto; 29 | border: $react-dates-width-tooltip-arrow / 2 solid transparent; 30 | border-top: 0; 31 | left: 22px; 32 | z-index: 2; 33 | } 34 | 35 | .DateInput--with-caret::before { 36 | top: $caret-top; 37 | border-bottom-color: rgba(0, 0, 0, 0.1); 38 | } 39 | 40 | .DateInput--with-caret::after { 41 | top: $caret-top + 1; 42 | border-bottom-color: $react-dates-color-white; 43 | } 44 | 45 | .DateInput--disabled { 46 | background: $react-dates-color-gray-lighter; 47 | } 48 | 49 | .DateInput__input { 50 | opacity: 0; 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | border: 0; 55 | height: 100%; 56 | width: 70%; 57 | 58 | &[readonly] { 59 | -moz-user-select: none; 60 | -webkit-user-select: none; 61 | -ms-user-select: none; 62 | user-select: none; 63 | } 64 | } 65 | 66 | .DateInput__display-text { 67 | padding: 4px 8px; 68 | white-space: nowrap; 69 | overflow: hidden; 70 | } 71 | 72 | .DateInput__display-text--has-input { 73 | color: $react-dates-color-text; 74 | } 75 | 76 | .DateInput__display-text--focused { 77 | background: $react-dates-color-focus; 78 | border-color: $react-dates-color-focus; 79 | border-radius: 3px; 80 | color: $react-dates-color-text-focus; 81 | } 82 | 83 | .DateInput__display-text--disabled { 84 | font-style: italic; 85 | } 86 | -------------------------------------------------------------------------------- /frontend/components/login/login_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | 4 | class LoginForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | email: "", 9 | password: "" 10 | }; 11 | this.handleSubmit = this.handleSubmit.bind(this); 12 | } 13 | 14 | handleSubmit(e) { 15 | e.preventDefault(); 16 | const user = Object.assign({}, this.state); 17 | this.props.processForm(user).then(() => this.props.deactivate('login')); 18 | } 19 | 20 | linkState(key) { 21 | return (event => this.setState({[key]: event.currentTarget.value})); 22 | } 23 | 24 | renderErrors() { 25 | if (this.props.errors) { 26 | const errors = this.props.errors[0]; 27 | return errors; 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | this.props.clear(); 33 | } 34 | 35 | render() { 36 | return( 37 |
38 |
    39 | { this.renderErrors() } 40 |
41 |
42 | 47 |
48 | 51 | 52 | 55 | 56 | 57 |
58 |
59 |
60 |

Don't have an account?

61 | 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | export default withRouter(LoginForm); 69 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # Schema Information 2 | 3 | ## users 4 | column name | data type | details 5 | -----------------|-----------|------------------------ 6 | id | integer | not null, primary key 7 | email | string | not null, indexed, unique 8 | fname | string | not null 9 | lname | string | not null 10 | image_url | string | not null 11 | location | string | 12 | password_digest | integer | not null 13 | session_token | integer | not null, indexed, unique 14 | 15 | ## spots 16 | column name | data type | details 17 | -----------------|-----------|------------------------ 18 | id | integer | not null, primary key 19 | owner_id | integer | not null, foreign key (references users), indexed 20 | featured | boolean | not null, indexed 21 | latitude | float | not null 22 | longitude | float | not null 23 | location | string | not null 24 | price | integer | not null, indexed 25 | guest_limit | integer | not null 26 | bedrooms | string | not null 27 | bathrooms | integer | not null 28 | beds | integer | not null 29 | image_url | string | not null, indexed 30 | description | text | not null 31 | 32 | 33 | ## bookings 34 | column name | data type | details 35 | -----------------|-----------|------------------------ 36 | id | integer | not null, primary key 37 | user_id | integer | not null, foreign key (references users), indexed 38 | spot_id | integer | not null, foreign key (references spots), indexed 39 | start_date | date | not null 40 | end_date | date | not null 41 | guest_number | integer | not null 42 | 43 | 44 | ## reviews 45 | column name | data type | details 46 | -----------------|-----------|------------------------ 47 | id | integer | not null, primary key 48 | author_id | integer | not null, foreign key (references users), indexed 49 | spot_id | integer | not null, foreign key (references spots), indexed 50 | rating | integer | not null 51 | body | string | not null 52 | -------------------------------------------------------------------------------- /frontend/components/review/review_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | 4 | class ReviewForm extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | rating: '', 10 | body: '' 11 | }; 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | this.update = this.update.bind(this); 14 | } 15 | 16 | componentWillReceiveProps(newprops) { 17 | this.clearInputs(); 18 | } 19 | 20 | clearInputs() { 21 | this.setState({ 22 | rating: '', 23 | body: '' 24 | }); 25 | } 26 | 27 | handleSubmit(e) { 28 | e.preventDefault(); 29 | const currentUser = this.props.currentUser; 30 | const rating = this.state.rating; 31 | const body = this.state.body; 32 | const spot = this.props.spot; 33 | const fullState = Object.assign(this.state, {spot_id: spot.id}); 34 | if ((rating > 0 && rating < 6) && body && currentUser) { 35 | this.clearInputs(); 36 | this.props.createReview(fullState); 37 | } 38 | } 39 | 40 | update(key) { 41 | return (event => this.setState({[key]: event.currentTarget.value})); 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 |
Write a Review
48 |
this.handleSubmit(e) } className="review-form"> 49 |
50 |
51 | 58 |
59 |
60 |
61 | 67 |
68 | 69 |
70 |
71 | ); 72 | } 73 | } 74 | 75 | export default withRouter(ReviewForm); 76 | -------------------------------------------------------------------------------- /app/assets/stylesheets/DayPicker.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .DayPicker { 4 | background: $react-dates-color-white; 5 | position: relative; 6 | text-align: left; 7 | } 8 | 9 | .DayPicker--horizontal { 10 | background: $react-dates-color-white; 11 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.07); 12 | border-radius: 3px; 13 | 14 | &.DayPicker--portal { 15 | box-shadow: none; 16 | position: absolute; 17 | left: 50%; 18 | top: 50%; 19 | } 20 | } 21 | 22 | .DayPicker--vertical.DayPicker--portal { 23 | position: initial; 24 | } 25 | 26 | // we can revisit this styling during the a11y picker 27 | .DayPicker__focus-region { 28 | outline: none; 29 | } 30 | 31 | .DayPicker__week-headers { 32 | position: relative; 33 | } 34 | 35 | .DayPicker--horizontal .DayPicker__week-headers { 36 | margin-left: 9px; 37 | } 38 | 39 | .DayPicker__week-header { 40 | color: $react-dates-color-placeholder-text; 41 | position: absolute; 42 | top: 62px; 43 | z-index: 2; 44 | padding: 0 13px; 45 | text-align: left; 46 | 47 | ul { 48 | list-style: none; 49 | margin: 1px 0; 50 | padding-left: 0; 51 | } 52 | 53 | li { 54 | display: inline-block; 55 | text-align: center; 56 | } 57 | } 58 | 59 | .DayPicker--vertical .DayPicker__week-header { 60 | left: 50%; 61 | } 62 | 63 | .DayPicker--vertical-scrollable { 64 | height: 100%; 65 | 66 | .DayPicker__week-header { 67 | top: 0; 68 | display: table-row; 69 | border-bottom: 1px solid $react-dates-color-border; 70 | background: white; 71 | } 72 | 73 | .transition-container--vertical { 74 | padding-top: 20px; 75 | height: 100%; 76 | position: absolute; 77 | top: 0; 78 | bottom: 0; 79 | right: 0; 80 | left: 0; 81 | overflow-y: scroll; 82 | } 83 | 84 | .DayPicker__week-header { 85 | margin-left: 0; 86 | left: 0; 87 | width: 100%; 88 | text-align: center; 89 | } 90 | } 91 | 92 | .transition-container { 93 | position: relative; 94 | overflow: hidden; 95 | border-radius: 3px; 96 | } 97 | 98 | .transition-container--horizontal { 99 | transition: height 0.2s ease-in-out; 100 | } 101 | 102 | .transition-container--vertical { 103 | width: 100%; 104 | } 105 | -------------------------------------------------------------------------------- /frontend/components/map/map.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { withRouter } from 'react-router'; 4 | import MarkerManager from '../../util/marker_manager'; 5 | 6 | class Map extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentDidMount() { 13 | let mapOptions = { 14 | center: {lat: 40.757433, lng: -73.985807}, 15 | zoom: 10 16 | }; 17 | this.map = new google.maps.Map(this.mapNode, mapOptions); 18 | 19 | if (typeof this.props.bounds.northeast.lat === 'number') { 20 | let latLngBounds = new google.maps.LatLngBounds(new google.maps.LatLng({lat: this.props.bounds.southwest.lat, lng: this.props.bounds.southwest.lng }), 21 | new google.maps.LatLng({lat: this.props.bounds.northeast.lat, lng: this.props.bounds.northeast.lng })); 22 | this.map.fitBounds(latLngBounds); 23 | } 24 | this.MarkerManager = new MarkerManager(this.map, this.handleMarkerClick.bind(this)); 25 | this.registerEventListeners(); 26 | this.MarkerManager.updateMarkers(this.props.spots); 27 | } 28 | 29 | componentDidUpdate() { 30 | this.MarkerManager.updateMarkers(this.props.spots); 31 | } 32 | 33 | componentWillReceiveProps(newprops) { 34 | 35 | if (newprops.address && newprops.address !== this.props.address) { 36 | let latLngBounds = new google.maps.LatLngBounds(new google.maps.LatLng({lat: newprops.bounds.southwest.lat, lng: newprops.bounds.southwest.lng }), 37 | new google.maps.LatLng({lat: newprops.bounds.northeast.lat, lng: newprops.bounds.northeast.lng })); 38 | 39 | this.map.fitBounds(latLngBounds); 40 | } 41 | 42 | } 43 | 44 | registerEventListeners() { 45 | google.maps.event.addListener(this.map, 'idle', () => { 46 | const { north, south, east, west } = this.map.getBounds().toJSON(); 47 | const bounds = { 48 | northeast: { lat: north, lng: east }, 49 | southwest: { lat: south, lng: west } }; 50 | 51 | if (!isNaN(bounds.northeast.lat)) { 52 | this.props.updateFilter([['bounds', bounds]]); 53 | } 54 | }); 55 | } 56 | 57 | handleMarkerClick(spots) { 58 | this.props.router.push(`/spots/${spots.id}`); 59 | } 60 | 61 | render() { 62 | return( 63 |
this.mapNode = map }/> 64 | ); 65 | } 66 | } 67 | 68 | export default withRouter(Map); 69 | -------------------------------------------------------------------------------- /app/controllers/api/bookings_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BookingsController < ApplicationController 2 | 3 | def create 4 | if start_date == nil 5 | render( 6 | json: ['Your booking must have a start date.'], 7 | status: 422 8 | ) 9 | elsif end_date == nil 10 | render( 11 | json: ['Your booking must have an end date.'], 12 | status: 422 13 | ) 14 | elsif guests == 0 15 | render( 16 | json: ['There must be at least one guest.'], 17 | status: 422 18 | ) 19 | elsif start_date != '' && end_date != '' && guests > 0 20 | parsed_start = Booking.date_convert(start_date) 21 | parsed_end = Booking.date_convert(end_date) 22 | @booking = Booking.new({ 23 | start_date: parsed_start, 24 | end_date: parsed_end, 25 | spot_id: spot_id, 26 | user_id: current_user.id, 27 | guest_number: guests 28 | }) 29 | 30 | no_overlap = Booking.no_overlap?(parsed_start, parsed_end, spot_id) 31 | guest_limit = Booking.guest_limit(spot_id) 32 | 33 | if no_overlap && guest_limit >= guests && @booking.save 34 | render :show 35 | elsif no_overlap == false && guest_limit < guests 36 | render( 37 | json: [ 38 | 'This booking conflicts with another. Try another date.', 39 | 'There are too many guests for this location.' 40 | ], 41 | status: 422 42 | ) 43 | elsif no_overlap == false 44 | render( 45 | json: ['This booking conflicts with another. Try another date.'], 46 | status: 422 47 | ) 48 | elsif guest_limit < guests 49 | render( 50 | json: ['There are too many guests for this location.'], 51 | status: 422 52 | ) 53 | end 54 | end 55 | 56 | end 57 | 58 | def index 59 | @bookings = Booking.find_bookings(current_user.id) 60 | render :index 61 | end 62 | 63 | private 64 | def booking_params 65 | params.require(:booking).permit(:start_date, :end_date, :guest_number, :spot_id, :user_id) 66 | end 67 | 68 | def start_date 69 | params["booking"]["start_date"] 70 | end 71 | 72 | def end_date 73 | params["booking"]["end_date"] 74 | end 75 | 76 | def guests 77 | params["booking"]["guest_number"].to_i 78 | end 79 | 80 | def spot_id 81 | params["booking"]["spot_id"].to_i 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /frontend/components/navigation/navigation_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import SearchBarContainer from './search_bar_container'; 4 | 5 | class NavigationBar extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | const user = this.props.currentUser; 13 | if (user) { 14 | return ( 15 | 40 | ); 41 | } else { 42 | return ( 43 | 69 | ); 70 | } 71 | } 72 | } 73 | 74 | export default NavigationBar; 75 | -------------------------------------------------------------------------------- /app/assets/stylesheets/DayPickerNavigation.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .DayPickerNavigation__prev, 4 | .DayPickerNavigation__next { 5 | cursor: pointer; 6 | line-height: 0.78; 7 | -webkit-user-select: none; /* Chrome/Safari */ 8 | -moz-user-select: none; /* Firefox */ 9 | -ms-user-select: none; /* IE10+ */ 10 | user-select: none; 11 | } 12 | 13 | .DayPickerNavigation__prev--default, 14 | .DayPickerNavigation__next--default { 15 | border: 1px solid $react-dates-color-border-light; 16 | background-color: $react-dates-color-white; 17 | color: $react-dates-color-placeholder-text; 18 | 19 | &:focus, 20 | &:hover { 21 | border: 1px solid $react-dates-color-border-medium; 22 | } 23 | 24 | &:active { 25 | background: darken($react-dates-color-white, 5%); 26 | } 27 | } 28 | 29 | .DayPickerNavigation--horizontal { 30 | position: relative; 31 | 32 | .DayPickerNavigation__prev, 33 | .DayPickerNavigation__next { 34 | border-radius: 3px; 35 | padding: 6px 9px; 36 | top: 18px; 37 | z-index: 2; 38 | position: absolute; 39 | } 40 | 41 | .DayPickerNavigation__prev { 42 | left: 22px; 43 | } 44 | 45 | .DayPickerNavigation__next { 46 | right: 22px; 47 | } 48 | 49 | .DayPickerNavigation__prev--default, 50 | .DayPickerNavigation__next--default { 51 | svg { 52 | height: 19px; 53 | width: 19px; 54 | fill: $react-dates-color-gray-light; 55 | } 56 | } 57 | } 58 | 59 | .DayPickerNavigation--vertical { 60 | background: $react-dates-color-white; 61 | box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1); 62 | position: absolute; 63 | bottom: 0; 64 | left: 0; 65 | height: 52px; 66 | width: 100%; 67 | z-index: 2; 68 | 69 | .DayPickerNavigation__prev, 70 | .DayPickerNavigation__next { 71 | display: inline-block; 72 | position: relative; 73 | height: 100%; 74 | width: 50%; 75 | } 76 | 77 | .DayPickerNavigation__next--default { 78 | border-left: 0; 79 | } 80 | 81 | .DayPickerNavigation__prev--default, 82 | .DayPickerNavigation__next--default { 83 | text-align: center; 84 | font-size: 2.5em; 85 | padding: 5px; 86 | 87 | svg { 88 | height: 42px; 89 | width: 42px; 90 | fill: $react-dates-color-text 91 | } 92 | } 93 | } 94 | 95 | .DayPickerNavigation--vertical-scrollable { 96 | position: relative; 97 | 98 | .DayPickerNavigation__next { 99 | width: 100%; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search.scss: -------------------------------------------------------------------------------- 1 | .search-page { 2 | display: flex; 3 | flex-direction: row-reverse; 4 | justify-content: space-between; 5 | align-items: flex-start; 6 | min-width: 1100px; 7 | } 8 | 9 | .search-spots { 10 | width: 60%; 11 | height: 915px; 12 | overflow: auto; 13 | display: flex; 14 | flex-direction: row; 15 | justify-content: center; 16 | } 17 | 18 | .all-spots { 19 | width: 90%; 20 | display: flex; 21 | flex-flow: row wrap; 22 | justify-content: space-around; 23 | align-items: space-around; 24 | } 25 | 26 | .all-spots > div { 27 | height: 340px; 28 | } 29 | 30 | .search-spots figure { 31 | display: flex; 32 | flex-direction: column; 33 | align-items: flex-start; 34 | justify-content: space-around; 35 | padding-top: 10px; 36 | width: 420px; 37 | height: 310px; 38 | } 39 | 40 | .search-query input { 41 | text-align: center; 42 | } 43 | 44 | .search-spots img { 45 | width: 400px; 46 | height: 275px; 47 | } 48 | 49 | .search-bar { 50 | width: 950px; 51 | height: 50px; 52 | } 53 | 54 | input[type=number]::-webkit-inner-spin-button, 55 | input[type=number]::-webkit-outer-spin-button { 56 | -webkit-appearance: none; 57 | -moz-appearance: none; 58 | appearance: none; 59 | margin: 0; 60 | } 61 | 62 | .search-query { 63 | width: 82%; 64 | height: 100%; 65 | display: flex; 66 | flex-direction: row; 67 | justify-content: center; 68 | } 69 | 70 | .search-submit-button { 71 | visibility: hidden; 72 | width: 0px; 73 | height: 0px; 74 | } 75 | 76 | .where { 77 | width: 30%; 78 | border-top: 1px solid #DCDCDC; 79 | border-left: 1px solid #DCDCDC; 80 | border-bottom: 1px solid #DCDCDC; 81 | box-shadow: 0 4px 2px -2px whitesmoke; 82 | border-top-left-radius: 4px; 83 | border-bottom-left-radius: 4px; 84 | display: flex; 85 | flex-direction: row; 86 | align-items: center; 87 | transition: width 0.3s, border-color 0.5s; 88 | } 89 | 90 | .how-many { 91 | width: 30%; 92 | border-top: 1px solid #DCDCDC; 93 | border-right: 1px solid #DCDCDC; 94 | border-bottom: 1px solid #DCDCDC; 95 | box-shadow: 0 4px 2px -2px whitesmoke; 96 | border-top-right-radius: 4px; 97 | border-bottom-right-radius: 4px; 98 | display: flex; 99 | flex-direction: row; 100 | align-items: center; 101 | transition: border-color 0.5s; 102 | } 103 | 104 | .where:focus { 105 | border-bottom: 2px solid #008984; 106 | } 107 | 108 | .how-many:focus { 109 | border-bottom: 2px solid #008984; 110 | } 111 | -------------------------------------------------------------------------------- /app/assets/stylesheets/booking.scss: -------------------------------------------------------------------------------- 1 | .booking-container { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .booking-form { 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: space-around; 13 | } 14 | 15 | .booking-price{ 16 | font-size: 20px; 17 | } 18 | 19 | .calendar-label { 20 | width: 90%; 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-around; 24 | } 25 | 26 | .numbers { 27 | width: 95%; 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: center; 31 | } 32 | 33 | .guest-number { 34 | width: 90%; 35 | height: 45px; 36 | border: 1px solid #DCDCDC; 37 | text-align: center; 38 | } 39 | 40 | .booking-submit { 41 | background-color: #FF5A5F; 42 | color: #fff; 43 | font-weight: 300; 44 | padding: 20px 100px 20px 100px; 45 | border-radius: 5px; 46 | } 47 | 48 | .booking-submit:hover { 49 | cursor: pointer; 50 | } 51 | 52 | .booking-errors { 53 | width: 90%; 54 | } 55 | 56 | .booking-errors ul { 57 | color: red; 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: center; 61 | align-items: center; 62 | } 63 | 64 | .bookings-page { 65 | width: 100%; 66 | display: flex; 67 | flex-direction: column; 68 | } 69 | 70 | .all-bookings { 71 | display: flex; 72 | flex-flow: row wrap; 73 | justify-content: space-around; 74 | } 75 | 76 | .bookings-page-title { 77 | font-size: 26px; 78 | color: #676767; 79 | font-weight: 600; 80 | padding: 10px 0px 10px 80px; 81 | } 82 | 83 | .individual-spot-container { 84 | display: flex; 85 | flex-direction: column; 86 | align-items: center; 87 | } 88 | 89 | .individual-spot { 90 | width: 100%; 91 | height: 100%; 92 | display: flex; 93 | flex-direction: column; 94 | align-items: center; 95 | margin: 10px; 96 | } 97 | 98 | .individual-spot img { 99 | width: 100%; 100 | height: 100%; 101 | } 102 | 103 | #book-link { 104 | font-size: 14px; 105 | font-weight: 400; 106 | text-decoration: none; 107 | outline: none; 108 | color: #676767; 109 | padding-bottom: 5.5px; 110 | border-bottom: 2px solid transparent; 111 | } 112 | 113 | #book-link:hover { 114 | border-bottom: 2px solid #676767; 115 | cursor: pointer; 116 | padding-bottom: 5.5px; 117 | } 118 | 119 | .booking-spot-image { 120 | width: 400px; 121 | height: 275px; 122 | } 123 | 124 | .spot-desc, .booking-range { 125 | font-size: 17px; 126 | color: #676767; 127 | font-weight: 400; 128 | } 129 | -------------------------------------------------------------------------------- /frontend/components/carousel/spot_carousel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slider from 'react-slick'; 3 | import { Link } from 'react-router'; 4 | import SpotIndexItem from '../spots/spot_index_item'; 5 | 6 | class SpotCarousel extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentDidMount() { 13 | this.props.fetchSpots(); 14 | } 15 | 16 | render() { 17 | //fix this so that featured homes is a separate query entirely 18 | const spots = Object.values(this.props.spots); 19 | const settings = { 20 | infinite: false, 21 | speed: 500, 22 | slidesToShow: 3, 23 | slidesToScroll: 1, 24 | arrows: true, 25 | draggable: false 26 | }; 27 | if (spots.length > 0) { 28 | const featuredSpots = spots.slice(0, 6); 29 | const otherSpots = spots.slice(6, spots.length); 30 | if (featuredSpots.length === 6 && otherSpots.length === (spots.length - 6)) { 31 | return ( 32 |
33 |
34 |
35 |
36 | Featured Homes 37 |
38 |
39 | See Map 40 |
41 |
42 |
43 | 44 | {featuredSpots.map((spot, idx) => { 45 | return
; 46 | }) 47 | } 48 |
49 |
50 |
51 |
52 |
53 |
54 | More Homes 55 |
56 |
57 | See Map 58 |
59 |
60 |
61 | 62 | {otherSpots.map((spot, idx) => { 63 | return
; 64 | }) 65 | } 66 |
67 |
68 |
69 |
70 | ); 71 | } else { 72 | return ( 73 |
74 | ); 75 | } 76 | } else { 77 | return ( 78 |
79 | ); 80 | } 81 | } 82 | } 83 | 84 | export default SpotCarousel; 85 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: 5 23 | 24 | development: 25 | <<: *default 26 | database: BillionairBnB_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | #username: BillionairBnB 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: BillionairBnB_test 61 | 62 | # As with config/secrets.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: BillionairBnB_production 84 | username: BillionairBnB 85 | password: <%= ENV['BILLIONAIRBNB_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /app/assets/stylesheets/modal.scss: -------------------------------------------------------------------------------- 1 | .modal-screen { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | z-index: 999; 8 | background: rgba(0, 0, 0, 0.7); 9 | } 10 | 11 | .modal-content { 12 | position: absolute; 13 | z-index: 1000; 14 | top: 50%; 15 | left: 50%; 16 | width: 500px; 17 | max-width: 100%; 18 | padding: 50px; 19 | margin-left: -300px; 20 | transform: translateY(-50%); 21 | background: white; 22 | border-radius: 5px; 23 | } 24 | 25 | .signup-main, .login-main { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: space-around; 29 | align-items: center; 30 | } 31 | 32 | .signup-form, .login-form { 33 | width: 100%; 34 | height: 100%; 35 | } 36 | 37 | .signup-form input, .login-form input { 38 | text-align: center; 39 | } 40 | 41 | .signup-divider { 42 | width: 100%; 43 | color: #DCDCDC; 44 | } 45 | 46 | .signup-errors { 47 | color: red; 48 | } 49 | 50 | .login-errors { 51 | color: red; 52 | } 53 | 54 | .signup-password-errors { 55 | color: red; 56 | padding-bottom: 7px; 57 | list-style: none; 58 | } 59 | 60 | .login-submit, .signup-submit, .demo-login { 61 | width: 100%; 62 | background-color: #FF5A5F; 63 | color: white; 64 | padding-top: 10px; 65 | padding-bottom: 10px; 66 | text-align: center; 67 | border-radius: 4px; 68 | cursor: pointer; 69 | } 70 | 71 | .login-submit, .demo-login { 72 | margin-top: 8px; 73 | margin-bottom: 8px; 74 | } 75 | 76 | .signup{ 77 | border: 1px solid #C0C0C0; 78 | margin-top: 8px; 79 | margin-bottom: 8px; 80 | } 81 | 82 | .login{ 83 | border: 1px solid #C0C0C0; 84 | margin-top: 8px; 85 | margin-bottom: 8px; 86 | } 87 | 88 | .signup[type="text"] { 89 | padding-top: 10px; 90 | padding-bottom: 10px; 91 | width: 100%; 92 | } 93 | 94 | .login[type="text"] { 95 | padding-top: 10px; 96 | padding-bottom: 10px; 97 | width: 100%; 98 | } 99 | 100 | .signup[type="password"] { 101 | padding-top: 10px; 102 | padding-bottom: 10px; 103 | width: 100%; 104 | } 105 | 106 | .login[type="password"] { 107 | padding-top: 10px; 108 | padding-bottom: 10px; 109 | width: 100%; 110 | } 111 | 112 | .switch-to-signup { 113 | width: 100%; 114 | display: flex; 115 | flex-direction: row; 116 | justify-content: space-between; 117 | align-items: center; 118 | color: #676767; 119 | } 120 | 121 | .switch-to-login { 122 | width: 100%; 123 | display: flex; 124 | flex-direction: row; 125 | justify-content: space-between; 126 | color: #676767; 127 | align-items: center; 128 | } 129 | 130 | .switch-to-login button { 131 | padding: 8px 5px 8px 5px; 132 | border: 2px solid #008489; 133 | border-radius: 4px; 134 | color: #008489; 135 | font-weight: 400; 136 | text-align: center; 137 | cursor: pointer; 138 | } 139 | 140 | .switch-to-signup button { 141 | padding: 8px 5px 8px 5px; 142 | border: 2px solid #008489; 143 | border-radius: 4px; 144 | color: #008489; 145 | font-weight: 400; 146 | text-align: center; 147 | cursor: pointer; 148 | } 149 | 150 | .switch-to-signup p { 151 | color: #676767 152 | } 153 | 154 | .switch-to-login p { 155 | color: #676767 156 | } 157 | 158 | .switch { 159 | width: 90px; 160 | } 161 | -------------------------------------------------------------------------------- /frontend/components/navigation/search_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DateRangePicker } from 'react-dates'; 3 | import { fetchBounds } from '../../util/search_api_util'; 4 | import { START_DATE, END_DATE } from 'react-dates/constants'; 5 | import { withRouter } from 'react-router'; 6 | 7 | class SearchBar extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | address: '', 13 | startDate: this.props.startDate, 14 | endDate: this.props.endDate, 15 | guests: '' 16 | }; 17 | this.handleSubmit = this.handleSubmit.bind(this); 18 | this.update = this.update.bind(this); 19 | } 20 | 21 | componentWillReceiveProps(newProps) { 22 | if (this.props.location.pathname !== newProps.location.pathname && newProps.location.pathname !== '/search') { 23 | this.setState({ 24 | address: '', 25 | startDate: this.props.startDate, 26 | endDate: this.props.endDate, 27 | guests: '' 28 | }); 29 | } 30 | } 31 | 32 | handleSubmit(e) { 33 | e.preventDefault(); 34 | let address = this.state.address; 35 | let guests = this.state.guests === '' ? 0 : this.state.guests; 36 | let startDate = this.formatMoment(this.state.startDate); 37 | let endDate = this.formatMoment(this.state.endDate); 38 | 39 | fetchBounds(address).then(gmaps => { 40 | if (!!gmaps.results[0].geometry.viewport) { 41 | this.props.updateFilter([ 42 | ['bounds', gmaps.results[0].geometry.viewport], 43 | ['guests', guests], 44 | ['startDate', startDate], 45 | ['endDate', endDate], 46 | ['address', gmaps.results[0].formatted_address] 47 | ]); 48 | } 49 | }).then(() => this.props.router.push('/search')); 50 | } 51 | 52 | update(key) { 53 | return ((e) => { 54 | this.setState({[key]: e.currentTarget.value}); 55 | }); 56 | } 57 | 58 | formatMoment(moment) { 59 | if (moment !== '') { 60 | let momentString = moment.format('YYYY,MM,DD'); 61 | return momentString; 62 | } 63 | } 64 | 65 | render() { 66 | return( 67 |
68 |
this.handleSubmit(e)}> 69 | 75 | this.setState({ startDate, endDate })} 79 | focusedInput={ this.state.focusedInput } 80 | onFocusChange={ focusedInput => this.setState({ focusedInput }) } /> 81 | 87 | 90 | 91 |
92 | ); 93 | } 94 | } 95 | 96 | export default withRouter(SearchBar); 97 | -------------------------------------------------------------------------------- /app/assets/stylesheets/CalendarDay.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .CalendarDay { 4 | border: 1px solid lighten($react-dates-color-border-light, 3); 5 | padding: 0; 6 | box-sizing: border-box; 7 | color: $react-dates-color-gray; 8 | cursor: pointer; 9 | } 10 | 11 | .CalendarDay__button { 12 | position: relative; 13 | height: 100%; 14 | width: 100%; 15 | text-align: center; 16 | background: none; 17 | border: 0; 18 | margin: 0; 19 | padding: 0; 20 | color: inherit; 21 | font: inherit; 22 | line-height: normal; 23 | overflow: visible; 24 | cursor: pointer; 25 | box-sizing: border-box; 26 | 27 | &:active { 28 | background: darken($react-dates-color-white, 5%); 29 | outline: 0; 30 | } 31 | } 32 | 33 | // This order is important. 34 | .CalendarDay--highlighted-calendar { 35 | background: $react-dates-color-highlighted; 36 | color: $react-dates-color-gray; 37 | cursor: default; 38 | 39 | &:active { 40 | background: $react-dates-color-secondary; 41 | } 42 | } 43 | 44 | .CalendarDay--outside { 45 | border: 0; 46 | cursor: default; 47 | 48 | &:active { 49 | background: $react-dates-color-white; 50 | } 51 | } 52 | 53 | .CalendarDay--hovered { 54 | background: lighten($react-dates-color-border-light, 3); 55 | border: 1px double darken($react-dates-color-border-light, 3); 56 | color: inherit; 57 | } 58 | 59 | .CalendarDay--blocked-minimum-nights { 60 | color: $react-dates-color-gray-lighter; 61 | background: $react-dates-color-white; 62 | border: 1px solid lighten($react-dates-color-border-light, 3); 63 | cursor: default; 64 | 65 | &:active { 66 | background: $react-dates-color-white; 67 | } 68 | } 69 | 70 | .CalendarDay--selected-span { 71 | background: $react-dates-color-primary-shade-2; 72 | border: 1px double $react-dates-color-primary-shade-1; 73 | color: $react-dates-color-white; 74 | 75 | &.CalendarDay--hovered, 76 | &:active { 77 | background: $react-dates-color-primary-shade-1; 78 | border: 1px double $react-dates-color-primary; 79 | } 80 | 81 | &.CalendarDay--last-in-range { 82 | border-right: $react-dates-color-primary; 83 | } 84 | } 85 | 86 | .CalendarDay--hovered-span, 87 | .CalendarDay--after-hovered-start { 88 | background: $react-dates-color-primary-shade-4; 89 | border: 1px double $react-dates-color-primary-shade-3; 90 | color: $react-dates-color-secondary; 91 | } 92 | 93 | .CalendarDay--selected-start, 94 | .CalendarDay--selected-end, 95 | .CalendarDay--selected { 96 | background: $react-dates-color-primary; 97 | border: 1px double $react-dates-color-primary; 98 | color: $react-dates-color-white; 99 | 100 | &:active { 101 | background: $react-dates-color-primary; 102 | } 103 | } 104 | 105 | .CalendarDay--blocked-calendar { 106 | background: $react-dates-color-gray-lighter; 107 | color: $react-dates-color-gray-light; 108 | cursor: default; 109 | 110 | &:active { 111 | background: $react-dates-color-gray-lighter; 112 | } 113 | } 114 | 115 | .CalendarDay--blocked-out-of-range { 116 | color: $react-dates-color-gray-lighter; 117 | background: $react-dates-color-white; 118 | border: 1px solid lighten($react-dates-color-border-light, 3); 119 | cursor: default; 120 | 121 | &:active { 122 | background: $react-dates-color-white; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20170428142732) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | create_table "bookings", force: :cascade do |t| 20 | t.integer "user_id", null: false 21 | t.integer "spot_id", null: false 22 | t.date "start_date", null: false 23 | t.date "end_date", null: false 24 | t.integer "guest_number", null: false 25 | t.datetime "created_at", null: false 26 | t.datetime "updated_at", null: false 27 | end 28 | 29 | add_index "bookings", ["spot_id"], name: "index_bookings_on_spot_id", using: :btree 30 | add_index "bookings", ["user_id"], name: "index_bookings_on_user_id", using: :btree 31 | 32 | create_table "reviews", force: :cascade do |t| 33 | t.integer "author_id", null: false 34 | t.integer "spot_id", null: false 35 | t.integer "rating", null: false 36 | t.string "body", null: false 37 | t.datetime "created_at", null: false 38 | t.datetime "updated_at", null: false 39 | end 40 | 41 | add_index "reviews", ["author_id"], name: "index_reviews_on_author_id", using: :btree 42 | add_index "reviews", ["spot_id"], name: "index_reviews_on_spot_id", using: :btree 43 | 44 | create_table "spots", force: :cascade do |t| 45 | t.float "lat", null: false 46 | t.float "lng", null: false 47 | t.integer "owner_id", null: false 48 | t.integer "price", null: false 49 | t.string "location", null: false 50 | t.string "image_url", null: false 51 | t.text "description", null: false 52 | t.datetime "created_at", null: false 53 | t.datetime "updated_at", null: false 54 | t.integer "guest_limit", null: false 55 | t.string "rules", null: false 56 | end 57 | 58 | add_index "spots", ["image_url"], name: "index_spots_on_image_url", using: :btree 59 | add_index "spots", ["owner_id"], name: "index_spots_on_owner_id", using: :btree 60 | add_index "spots", ["price"], name: "index_spots_on_price", using: :btree 61 | 62 | create_table "users", force: :cascade do |t| 63 | t.string "email", null: false 64 | t.string "password_digest", null: false 65 | t.string "session_token", null: false 66 | t.datetime "created_at", null: false 67 | t.datetime "updated_at", null: false 68 | t.string "fname", null: false 69 | t.string "lname", null: false 70 | end 71 | 72 | add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree 73 | 74 | end 75 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /frontend/components/booking/booking.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DateRangePicker } from 'react-dates'; 3 | import { withRouter } from 'react-router'; 4 | import { START_DATE, END_DATE } from 'react-dates/constants'; 5 | 6 | class Booking extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.handleSubmit = this.handleSubmit.bind(this); 11 | this.state = { 12 | startDate: '', 13 | endDate: '', 14 | guests: 0 15 | }; 16 | } 17 | 18 | componentWillReceiveProps(newProps) { 19 | if(this.props.errors.length > 0) { 20 | this.props.clear(); 21 | } 22 | } 23 | 24 | componentDidMount() { 25 | if(this.props.errors.length > 0) { 26 | this.props.clear(); 27 | } 28 | } 29 | 30 | handleSubmit(e) { 31 | e.preventDefault(); 32 | this.props.clear(); 33 | let startDate = this.formatMoment(this.state.startDate); 34 | let endDate = this.formatMoment(this.state.endDate); 35 | if (this.props.currentUser) { 36 | this.props.createBooking({ 37 | start_date: startDate, 38 | end_date: endDate, 39 | spot_id: this.props.spot.id, 40 | guest_number: parseInt(this.state.guests) 41 | }).then(() => this.props.router.push(`/bookings`)); 42 | } 43 | } 44 | 45 | update(key) { 46 | return (event => this.setState({[key]: event.currentTarget.value})); 47 | } 48 | 49 | formatMoment(moment) { 50 | if (moment !== '') { 51 | let momentString = moment.format('YYYY,MM,DD'); 52 | return momentString; 53 | } 54 | } 55 | 56 | dateRange(momentA, momentB) { 57 | if (momentA !== '' && momentB !== '') { 58 | return momentB.diff(momentA, 'days'); 59 | } else { 60 | return 0; 61 | } 62 | } 63 | 64 | renderErrors() { 65 | if (!this.props.currentUser) { 66 | return (
  • Not Logged In

  • ); 67 | } else if (this.props.errors) { 68 | return (this.props.errors.map((err, idx) => { 69 | return (
  • { err }
  • ); 70 | })); 71 | } 72 | } 73 | 74 | render() { 75 | const price = this.props.spot.price; 76 | const dateRange = this.dateRange(this.state.startDate, this.state.endDate); 77 | const guests = this.state.guests; 78 | return( 79 |
    80 |
    this.handleSubmit(e) }> 81 |
    82 | ${ this.props.spot.price} per night 83 |
    84 |
    85 |

    Check In

    86 |

    Check Out

    87 |
    88 |
    89 | this.setState({ startDate, endDate })} 93 | focusedInput={ this.state.focusedInput } 94 | onFocusChange={ focusedInput => this.setState({ focusedInput }) } /> 95 |
    96 |
    97 | 102 |
    103 |
    104 | ${ price } * { dateRange } days * { guests } guests = ${ dateRange * price * guests } 105 |
    106 |
    107 |
      108 | { this.renderErrors() } 109 |
    110 |
    111 | 112 |
    113 |
    114 | ); 115 | } 116 | 117 | } 118 | 119 | export default withRouter(Booking); 120 | -------------------------------------------------------------------------------- /frontend/components/signup/signup_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | 4 | class SignupForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | email: "", 9 | fname: "", 10 | lname: "", 11 | password: "" 12 | }; 13 | this.handleSubmit = this.handleSubmit.bind(this); 14 | } 15 | 16 | handleSubmit(e) { 17 | e.preventDefault(); 18 | const user = Object.assign({}, this.state); 19 | this.props.processForm(user).then(() => this.props.deactivate('signup')); 20 | } 21 | 22 | linkState(key) { 23 | return (event => this.setState({[key]: event.currentTarget.value})); 24 | } 25 | 26 | componentDidMount() { 27 | this.props.clear(); 28 | } 29 | 30 | emailErrors() { 31 | let emailErrors = ""; 32 | if (this.props.errors.email) { 33 | emailErrors = this.props.errors.email; 34 | emailErrors = emailErrors.map((err, idx) => { 35 | return (
  • {`Email ${err}`}
  • ); 36 | }); 37 | } 38 | return emailErrors; 39 | } 40 | 41 | fnameErrors() { 42 | let fnameErrors = ''; 43 | if (this.props.errors.fname) { 44 | fnameErrors = this.props.errors.fname; 45 | fnameErrors = fnameErrors.map((err, idx) => { 46 | return (
  • {`First Name ${err}`}
  • ); 47 | }); 48 | } 49 | return fnameErrors; 50 | } 51 | 52 | lnameErrors() { 53 | let lnameErrors = ''; 54 | if (this.props.errors.lname) { 55 | lnameErrors = this.props.errors.lname; 56 | lnameErrors = lnameErrors.map((err, idx) => { 57 | return (
  • {`Last Name ${err}`}
  • ); 58 | }); 59 | } 60 | return lnameErrors; 61 | } 62 | 63 | passwordErrors() { 64 | let passwordErrors = ''; 65 | if (this.props.errors.password) { 66 | passwordErrors = this.props.errors.password; 67 | passwordErrors = passwordErrors.map((err, idx) => { 68 | return (
  • {`Password ${err}`}
  • ); 69 | }); 70 | } 71 | return passwordErrors; 72 | } 73 | 74 | render() { 75 | return( 76 |
    77 |
    78 | 81 |
      82 | { this.emailErrors() } 83 |
    84 | 87 |
      88 | { this.fnameErrors() } 89 |
    90 | 93 |
      94 | { this.lnameErrors() } 95 |
    96 | 99 |
      100 | { this.passwordErrors() } 101 |
    102 | 103 |
    104 |
    105 |
    106 |

    Already have a BillionairBnB account?

    107 | 108 |
    109 |
    110 | ); 111 | } 112 | } 113 | 114 | export default withRouter(SignupForm); 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BillionairBnB 2 | 3 | Link to live site: [BillionairBnB](https://billionairbnb.herokuapp.com/) 4 | 5 | BillionairBnB is an application that allows its users to stay at luxury homes around the world. It is inspired by Airbnb, and was built using a Ruby on Rails backend, the React/Redux framework on the frontend, and a PostgreSQL database. 6 | 7 | ![alt text](/app/assets/images/mainpage.png) 8 | 9 | ## Features & Implementation 10 | 11 | ### Spot Search Utilizing Google Maps API 12 | 13 | BillionairBnB has search functionality that gives users the ability to search for homes based on where in the world they want to visit. When they input a place, an API request is sent to the Google Geocoder API, which responds with coordinates that the map can use to render the correct area. From there, the database filters spots based on which ones are within the bounds of the map. Since this filter also works when users move the map themselves, its dynamism enhances the user experience greatly. 14 | 15 | ![alt text](/app/assets/images/searchpage.png) 16 | 17 | ```JavaScript 18 | class Map extends React.Component { 19 | componentDidMount() { 20 | let mapOptions = { 21 | center: {lat: 40.757433, lng: -73.985807}, 22 | zoom: 10 23 | }; 24 | this.map = new google.maps.Map(this.mapNode, mapOptions); 25 | 26 | if (typeof this.props.bounds.northeast.lat === 'number') { 27 | let latLngBounds = new google.maps.LatLngBounds(new google.maps.LatLng({lat: this.props.bounds.southwest.lat, lng: this.props.bounds.southwest.lng }), 28 | new google.maps.LatLng({lat: this.props.bounds.northeast.lat, lng: this.props.bounds.northeast.lng })); 29 | this.map.fitBounds(latLngBounds); 30 | } 31 | this.MarkerManager = new MarkerManager(this.map, this.handleMarkerClick.bind(this)); 32 | this.registerEventListeners(); 33 | this.MarkerManager.updateMarkers(this.props.spots); 34 | } 35 | } 36 | ``` 37 | 38 | In order to make this possible, the viewport coordinates are parsed from the Google Geocoder API response, and then used to reconfigure the map to the location that the user requested. Due to Google Maps' flexibility, BillionairBnB users can search anything from neighborhoods to countries. An initial problem I had while implementing this feature was that the Google Geocoder functions for moving the map, such as fitBounds, would set the bounds to be slightly wider than intended, which would be perceived as a difference. As a result, the map would endlessly try to fit itself to the new bounds. My solution was to focus on the address from the API response, since that always remained constant. 39 | 40 | ```JavaScript 41 | class Map extends React.Component { 42 | componentWillReceiveProps(newprops) { 43 | 44 | if (newprops.address && newprops.address !== this.props.address) { 45 | let latLngBounds = new google.maps.LatLngBounds(new google.maps.LatLng({lat: newprops.bounds.southwest.lat, lng: newprops.bounds.southwest.lng }), 46 | new google.maps.LatLng({lat: newprops.bounds.northeast.lat, lng: newprops.bounds.northeast.lng })); 47 | 48 | this.map.fitBounds(latLngBounds); 49 | } 50 | 51 | } 52 | } 53 | ``` 54 | 55 | ### Bookings 56 | 57 | When a user wants to book a spot to stay in, they use the dynamic React Dates calendar, which is a library maintained by Airbnb. After inputting the start and end dates of their stay, there is a database check that makes sure that there are no conflicting bookings. If there is any conflict, an error is raised, and the user knows to modify the dates of their stay. 58 | 59 | ```Ruby 60 | class Booking < ActiveRecord::Base 61 | def self.no_overlap?(book_start_date, book_end_date, spot_id) 62 | spot_bookings = Spot.find_by(id: spot_id).bookings 63 | spot_bookings.none? do |booking| 64 | !((booking.start_date > book_end_date) || (book_start_date > booking.end_date)) 65 | end 66 | end 67 | end 68 | ``` 69 | 70 | ## Future Features to be Added 71 | 72 | ### User/Host Profiles 73 | 74 | Allow users to create profiles and customize settings, such as location and profile picture 75 | 76 | ### Spot Creation 77 | 78 | Allow users to become host by placing their own spot on the site that others can book 79 | 80 | ### Messaging 81 | 82 | Facilitate communication between user and host through the site 83 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spots.scss: -------------------------------------------------------------------------------- 1 | .outer-carousel { 2 | width: 100%; 3 | height: 870px; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-around; 7 | align-items: center; 8 | } 9 | 10 | .featured-homes-carousel { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | 17 | .other-homes-carousel { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .carousel-header { 25 | height: 80px; 26 | width: 55.5%; 27 | min-width: 1065px; 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | align-items: center; 32 | } 33 | 34 | .inner-carousel { 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: center; 38 | align-items: center; 39 | width: 70%; 40 | } 41 | 42 | .searchlink { 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: space-between; 46 | align-items: center; 47 | } 48 | 49 | .spots-link { 50 | font-size: 14px; 51 | font-weight: 500; 52 | text-decoration: none; 53 | outline: none; 54 | color: #676767; 55 | } 56 | 57 | .carousel-header-title { 58 | font-size: 26px; 59 | color: #676767; 60 | font-weight: 600; 61 | } 62 | 63 | .slick-list { 64 | width: 1080px; 65 | } 66 | 67 | .carousel figure { 68 | display: block; 69 | width: 100%; 70 | } 71 | 72 | .spot-item { 73 | padding-left: 6px; 74 | padding-right: 6px; 75 | width: 350px; 76 | height: 255px; 77 | display: flex; 78 | flex-direction: column; 79 | justify-content: space-between; 80 | } 81 | 82 | .spot-item img { 83 | width: 350px; 84 | height: 230px; 85 | margin: 0; 86 | } 87 | 88 | .slick-slide a, .all-spots a { 89 | text-decoration: none; 90 | } 91 | 92 | .spot-show-page { 93 | display: flex; 94 | flex-direction: column; 95 | align-items: center; 96 | padding-bottom: 20px; 97 | height: 90%; 98 | min-width: 1300px; 99 | } 100 | 101 | .spot-image { 102 | width: 1250px; 103 | height: 620px; 104 | } 105 | 106 | .spot-image img { 107 | width: 100%; 108 | height: 100%; 109 | } 110 | 111 | .spot-information { 112 | width: 1000px; 113 | display: flex; 114 | flex-direction: row; 115 | justify-content: space-between; 116 | } 117 | 118 | .spot-details { 119 | width: 665px; 120 | font-size: 30px; 121 | font-weight: bold; 122 | font-family: 'Roboto', sans-serif; 123 | color: #484848; 124 | display: flex; 125 | flex-direction: column; 126 | align-items: flex-start; 127 | } 128 | 129 | .spot-details p { 130 | font-size: 16px; 131 | } 132 | 133 | .spot-booking { 134 | width: 315px; 135 | height: 400px; 136 | border-bottom: 1px solid #C0C0C0; 137 | border-left: 1px solid #C0C0C0; 138 | border-right: 1px solid #C0C0C0; 139 | } 140 | 141 | .spot-owner-description { 142 | padding-top: 15px; 143 | padding-bottom: 20px; 144 | height: 120px; 145 | display: flex; 146 | flex-direction: column; 147 | justify-content: space-around; 148 | } 149 | 150 | .spot-rules { 151 | border-top: 1px solid #C0C0C0; 152 | width: 100%; 153 | height: 100px; 154 | display: flex; 155 | flex-direction: column; 156 | justify-content: space-around; 157 | align-items: flex-start; 158 | } 159 | 160 | .rules-header { 161 | margin: 0; 162 | align-self: flex-start; 163 | } 164 | 165 | .rules { 166 | font-size: 16px; 167 | font-weight: 300; 168 | } 169 | 170 | .description { 171 | font-size: 15px; 172 | font-family: 'Roboto', sans-serif;; 173 | color: #676767; 174 | font-weight: 400; 175 | } 176 | 177 | .price { 178 | font-size: 15px; 179 | padding-right: 5px; 180 | font-family: 'Roboto', sans-serif;; 181 | color: #676767; 182 | font-weight: 600; 183 | } 184 | 185 | .slick-disabled { 186 | visibility: hidden; 187 | } 188 | 189 | .slick-prev { 190 | padding-right: 7px; 191 | } 192 | 193 | .slick-next { 194 | padding-left: 7px; 195 | } 196 | 197 | .slick-arrow, .slick-prev, .slick-next { 198 | color: transparent; 199 | width: 24px; 200 | height: 24px; 201 | align-self: center; 202 | } 203 | 204 | .slick-prev:before { 205 | content: image-url('Logomakr_0gqPjJ.png'); 206 | zoom: 8%; 207 | } 208 | 209 | .slick-next:before { 210 | content: image-url('Logomakr_6djnyC.png'); 211 | zoom: 8%; 212 | } 213 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_self 14 | */ 15 | @import '_datepicker'; 16 | @import 'CalendarDay'; 17 | @import 'CalendarMonth'; 18 | @import 'CalendarMonthGrid'; 19 | @import 'DayPicker'; 20 | @import 'DayPickerNavigation'; 21 | @import 'DateInput'; 22 | @import 'DateRangePicker'; 23 | @import 'DateRangePickerInput'; 24 | @import 'modal'; 25 | @import 'css_reset'; 26 | @import 'spots'; 27 | @import 'search'; 28 | @import 'booking'; 29 | @import 'review'; 30 | @import 'footer'; 31 | 32 | body { 33 | font-family: 'Roboto', sans-serif;; 34 | } 35 | 36 | #map-container { 37 | width: 775px; 38 | height: 915px; 39 | } 40 | 41 | #logo { 42 | content: image-url('bnb.png'); 43 | zoom: 22%; 44 | } 45 | 46 | .login-errors { 47 | list-style: none; 48 | } 49 | 50 | .signup-errors { 51 | list-style: none; 52 | } 53 | 54 | .homepage { 55 | min-width: 1200px; 56 | } 57 | 58 | .navbar { 59 | display: flex; 60 | flex-direction: row; 61 | align-items: flex-start; 62 | justify-content: center; 63 | padding-bottom: 10px; 64 | min-width: 1300px; 65 | } 66 | 67 | .navbar-left { 68 | padding-left: 26px; 69 | padding-top: 14px; 70 | } 71 | 72 | .navbar-middle-logged-out, .navbar-middle-logged-in { 73 | padding-top: 5px; 74 | align-self: center; 75 | display: flex; 76 | flex-direction: row; 77 | justify-content: flex-start; 78 | } 79 | 80 | .navbar-middle-logged-out { 81 | width: 88%; 82 | } 83 | 84 | .navbar-middle-logged-in { 85 | width: 80%; 86 | } 87 | 88 | .search-bar-shift { 89 | width: 100%; 90 | } 91 | 92 | .navbar-right-logged-out { 93 | width: 150px; 94 | padding: 18px 12px 18px 14px; 95 | display: flex; 96 | flex-direction: row; 97 | justify-content: space-around; 98 | } 99 | 100 | .navbar-right-logged-in { 101 | width: 320px; 102 | padding: 18px 12px 18px 14px; 103 | display: flex; 104 | flex-direction: row; 105 | justify-content: space-around; 106 | } 107 | 108 | .navbar-right-logged-out button, .navbar-right-logged-in button { 109 | color: #676767; 110 | width: 100%; 111 | height: 100%; 112 | text-decoration: none; 113 | background: none; 114 | border: none; 115 | padding: none; 116 | font-size: 14px; 117 | font-weight: 450; 118 | } 119 | 120 | .navbar-right-hover { 121 | height: 100%; 122 | padding: none; 123 | margin-bottom: 3px; 124 | border-bottom: 2px solid transparent; 125 | } 126 | 127 | .navbar-right button:focus { 128 | outline: none; 129 | } 130 | 131 | .navbar-right-hover:hover { 132 | border-bottom: 2px solid #676767; 133 | cursor: pointer; 134 | margin-bottom: 3px; 135 | } 136 | 137 | .greeting-1 { 138 | display: flex; 139 | flex-direction: row; 140 | justify-content: center; 141 | align-items: center; 142 | width: 100%; 143 | height: 180px; 144 | } 145 | 146 | .greeting-2 { 147 | width: 56%; 148 | min-width: 660px; 149 | height: 100%; 150 | display: flex; 151 | flex-direction: row; 152 | justify-content: flex-start; 153 | align-items: center; 154 | margin-top: 50px; 155 | } 156 | 157 | .greeting-3 { 158 | width: 60%; 159 | display: flex; 160 | flex-flow: row wrap; 161 | justify-content: flex-start; 162 | } 163 | 164 | .greeting-text-1 { 165 | color: #FF5A5F; 166 | font-size: 46px; 167 | font-weight: 600; 168 | padding-right: 5px; 169 | } 170 | 171 | .greeting-text-2 { 172 | color: #676767; 173 | font-size: 46px; 174 | font-weight: 200; 175 | } 176 | 177 | .user-greeting { 178 | font-size: 15px; 179 | color: #676767; 180 | } 181 | 182 | .footer-link { 183 | font-size: 16px; 184 | font-weight: 500; 185 | text-decoration: none; 186 | outline: none; 187 | color: #4c4747; 188 | cursor: pointer; 189 | } 190 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.8) 5 | actionpack (= 4.2.8) 6 | actionview (= 5.2.2) 7 | activejob (= 4.2.11) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.8) 11 | actionview (= 5.2.2) 12 | activesupport (= 4.2.11) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (4.2.8) 18 | activesupport (= 4.2.11) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 23 | activejob (4.2.11) 24 | activesupport (= 4.2.11) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.8) 27 | activesupport (= 4.2.11) 28 | builder (~> 3.1) 29 | activerecord (4.2.8) 30 | activemodel (= 4.2.8) 31 | activesupport (= 4.2.11) 32 | arel (~> 6.0) 33 | activesupport (4.2.11) 34 | i18n (~> 0.7) 35 | minitest (~> 5.1) 36 | thread_safe (~> 0.3, >= 0.3.4) 37 | tzinfo (~> 1.1) 38 | annotate (2.7.2) 39 | activerecord (>= 3.2, < 6.0) 40 | rake (>= 10.4, < 13.0) 41 | arel (6.0.4) 42 | bcrypt (3.1.11) 43 | better_errors (2.4.0) 44 | coderay (>= 1.0.0) 45 | erubi (>= 1.0.0) 46 | rack (>= 0.9.0) 47 | binding_of_caller (0.8.0) 48 | debug_inspector (>= 0.0.1) 49 | builder (3.2.3) 50 | byebug (10.0.1) 51 | coderay (1.1.2) 52 | coffee-rails (4.1.1) 53 | coffee-script (>= 2.2.0) 54 | railties (>= 4.0.0, < 5.1.x) 55 | coffee-script (2.4.1) 56 | coffee-script-source 57 | execjs 58 | coffee-script-source (1.12.2) 59 | concurrent-ruby (1.1.3) 60 | crass (1.0.4) 61 | debug_inspector (0.0.3) 62 | erubi (1.7.1) 63 | erubis (2.7.0) 64 | execjs (2.7.0) 65 | faker (1.8.7) 66 | i18n (>= 0.7) 67 | ffi (1.9.25) 68 | figaro (1.1.1) 69 | thor (~> 0.14) 70 | globalid (0.4.1) 71 | activesupport (>= 4.2.0) 72 | i18n (0.9.5) 73 | concurrent-ruby (~> 1.0) 74 | jbuilder (2.7.0) 75 | activesupport (>= 4.2.0) 76 | multi_json (>= 1.2) 77 | jquery-rails (4.3.1) 78 | rails-dom-testing (>= 1, < 3) 79 | railties (>= 4.2.0) 80 | thor (>= 0.14, < 2.0) 81 | json (1.8.6) 82 | loofah (2.2.3) 83 | crass (~> 1.0.2) 84 | nokogiri (>= 1.5.9) 85 | mail (2.7.0) 86 | mini_mime (>= 0.1.1) 87 | method_source (0.9.0) 88 | mini_mime (1.0.0) 89 | mini_portile2 (2.3.0) 90 | minitest (5.11.3) 91 | multi_json (1.13.1) 92 | nokogiri (1.8.5) 93 | mini_portile2 (~> 2.3.0) 94 | pg (0.21.0) 95 | pry (0.11.3) 96 | coderay (~> 1.1.0) 97 | method_source (~> 0.9.0) 98 | pry-rails (0.3.6) 99 | pry (>= 0.10.4) 100 | rack (1.6.11) 101 | rack-test (0.6.3) 102 | rack (>= 1.0) 103 | rails (4.2.8) 104 | actionmailer (= 4.2.8) 105 | actionpack (= 4.2.8) 106 | actionview (= 5.2.2) 107 | activejob (= 4.2.11) 108 | activemodel (= 4.2.8) 109 | activerecord (= 4.2.8) 110 | activesupport (= 4.2.11) 111 | bundler (>= 1.3.0, < 2.0) 112 | railties (= 4.2.8) 113 | sprockets-rails 114 | rails-deprecated_sanitizer (1.0.3) 115 | activesupport (>= 4.2.0.alpha) 116 | rails-dom-testing (1.0.9) 117 | activesupport (>= 4.2.0, < 5.0) 118 | nokogiri (~> 1.6) 119 | rails-deprecated_sanitizer (>= 1.0.1) 120 | rails-html-sanitizer (1.0.4) 121 | loofah (~> 2.2, >= 2.2.2) 122 | rails_12factor (0.0.3) 123 | rails_serve_static_assets 124 | rails_stdout_logging 125 | rails_serve_static_assets (0.0.5) 126 | rails_stdout_logging (0.0.5) 127 | railties (4.2.8) 128 | actionpack (= 4.2.8) 129 | activesupport (= 4.2.11) 130 | rake (>= 0.8.7) 131 | thor (>= 0.18.1, < 2.0) 132 | rake (12.3.1) 133 | rb-fsevent (0.10.3) 134 | rb-inotify (0.9.10) 135 | ffi (>= 0.5.0, < 2) 136 | rdoc (4.3.0) 137 | sass (3.5.6) 138 | sass-listen (~> 4.0.0) 139 | sass-listen (4.0.0) 140 | rb-fsevent (~> 0.9, >= 0.9.4) 141 | rb-inotify (~> 0.9, >= 0.9.7) 142 | sass-rails (5.0.7) 143 | railties (>= 4.0.0, < 6) 144 | sass (~> 3.1) 145 | sprockets (>= 2.8, < 4.0) 146 | sprockets-rails (>= 2.0, < 4.0) 147 | tilt (>= 1.1, < 3) 148 | sdoc (0.4.2) 149 | json (~> 1.7, >= 1.7.7) 150 | rdoc (~> 4.0) 151 | spring (2.0.2) 152 | activesupport (>= 4.2) 153 | sprockets (3.7.2) 154 | concurrent-ruby (~> 1.0) 155 | rack (> 1, < 3) 156 | sprockets-rails (3.2.1) 157 | actionpack (>= 4.0) 158 | activesupport (>= 4.0) 159 | sprockets (>= 3.0.0) 160 | thor (0.20.0) 161 | thread_safe (0.3.6) 162 | tilt (2.0.8) 163 | tzinfo (1.2.5) 164 | thread_safe (~> 0.1) 165 | uglifier (4.1.8) 166 | execjs (>= 0.3.0, < 3) 167 | validates_email_format_of (1.6.3) 168 | i18n 169 | web-console (2.3.0) 170 | activemodel (>= 4.0) 171 | binding_of_caller (>= 0.7.2) 172 | railties (>= 4.0) 173 | sprockets-rails (>= 2.0, < 4.0) 174 | 175 | PLATFORMS 176 | ruby 177 | 178 | DEPENDENCIES 179 | annotate 180 | bcrypt (~> 3.1.7) 181 | better_errors 182 | binding_of_caller 183 | byebug 184 | coffee-rails (~> 4.1.0) 185 | faker 186 | figaro 187 | jbuilder (~> 2.0) 188 | jquery-rails 189 | pg (~> 0.15) 190 | pry-rails 191 | rails (= 4.2.8) 192 | rails_12factor 193 | sass-rails (~> 5.0) 194 | sdoc (~> 0.4.0) 195 | spring 196 | uglifier (>= 1.3.0) 197 | validates_email_format_of 198 | web-console (~> 2.0) 199 | 200 | BUNDLED WITH 201 | 1.17.1 202 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | 9 | # demo_user = User.create(email: 'moktar@jama.com', fname: 'Moktar', lname: 'Jama', password: 'password') 10 | # demo_user_2 = User.create(email: 'joktar@mama.com', fname: 'Joktar', lname: 'Mama', password: 'dassworp') 11 | 12 | new_york = Spot.create( 13 | description: 'Amazing TriBeCa penthouse', 14 | image_url: 'https://www.thepinnaclelist.com/wp-content/uploads/2014/10/01-48.0-Million-Sky-Lofts-Glasshouse-Penthouse-145-Hudson-Street-New-York-NY-920x700.jpg', 15 | price: 800, 16 | location: 'New York City, NY, USA', 17 | owner_id: 1, 18 | lat: 40.721695, 19 | lng: -74.011635, 20 | guest_limit: 5, 21 | rules: Faker::StarWars.quote 22 | ) 23 | 24 | los_angeles = Spot.create( 25 | description: 'Great mansion in Bel-Air', 26 | image_url: 'https://s-media-cache-ak0.pinimg.com/originals/35/16/65/351665403bad01e7215aca7c2c6d67af.png', 27 | price: 1000, 28 | location: 'Los Angeles, CA, USA', 29 | owner_id: 1, 30 | lat: 34.099428, 31 | lng: -118.461763, 32 | guest_limit: 10, 33 | rules: "Your name can't be DJ Jazzy Jeff" 34 | ) 35 | 36 | london = Spot.create( 37 | description: 'Absolutely regal palace', 38 | image_url: 'https://www.royal.uk/sites/default/files/images/feature/buckingham-palace.jpg', 39 | price: 1000, 40 | location: 'London, England, UK', 41 | owner_id: 1, 42 | lat: 51.501344, 43 | lng: 0.141890, 44 | guest_limit: 2, 45 | rules: Faker::Hipster.sentence 46 | ) 47 | 48 | kanazawa = Spot.create( 49 | description: 'Historic and gorgeous castle', 50 | image_url: 'https://upload.wikimedia.org/wikipedia/commons/c/cd/Kanazawa-M-5935.jpg', 51 | price: 800, 52 | location: 'Kanazawa, Ishikawa, Japan', 53 | owner_id: 1, 54 | lat: 36.563950, 55 | lng: 136.659507, 56 | guest_limit: 40, 57 | rules: Faker::ChuckNorris.fact 58 | ) 59 | 60 | dubai = Spot.create( 61 | description: 'man-made island for you and your friends to enjoy', 62 | image_url: 'https://ak3.picdn.net/shutterstock/videos/3730241/thumb/1.jpg?i10c=img.resize(height:72)', 63 | price: 800, 64 | location: 'Dubai, UAE', 65 | owner_id: 1, 66 | lat: 25.112953, 67 | lng: 55.138778, 68 | guest_limit: 30, 69 | rules: Faker::StarWars.quote 70 | ) 71 | 72 | santorini = Spot.create( 73 | description: 'seashore villa with an amazing view', 74 | image_url: 'https://t-ec.bstatic.com/images/hotel/max1024x768/731/73116664.jpg', 75 | price: 700, 76 | location: 'Santorini, Greece', 77 | owner_id: 1, 78 | lat: 36.475422, 79 | lng: 25.413763, 80 | guest_limit: 7, 81 | rules: Faker::StarWars.quote 82 | ) 83 | 84 | singapore = Spot.create( 85 | description: 'modern penthouse in a thriving Singaporean district', 86 | image_url: 'https://sg1-cdn.pgimgs.com/listing/18888255/UPHO.53580206.V800/New-Penthouse-Jurong-Amazing-Water-Views%21-Boon-Lay-Jurong-Tuas-Singapore.jpg', 87 | price: 800, 88 | location: 'Singapore, Singapore', 89 | owner_id: 1, 90 | lat: 1.305224, 91 | lng: 103.913765, 92 | guest_limit: 4, 93 | rules: Faker::ChuckNorris.fact 94 | ) 95 | 96 | san_francisco = Spot.create( 97 | description: 'excellent getaway for tech billionaires', 98 | image_url: 'https://ap.rdcpix.com/1771840340/12a1ef9c744719a43a4890b2aee25f0fl-m0xd-w480_h480_q80.jpg', 99 | price: 1000, 100 | location: 'San Francisco, CA, USA', 101 | owner_id: 1, 102 | lat: 37.788497, 103 | lng: -122.457513, 104 | guest_limit: 11, 105 | rules: Faker::Hipster.sentence 106 | ) 107 | 108 | hong_kong = Spot.create( 109 | description: 'an amazing getaway from the bustling metropolis', 110 | image_url: 'https://www.engelvoelkers.com/wp-content/uploads/2014/11/Capture.png', 111 | price: 1200, 112 | location: 'Hong Kong', 113 | owner_id: 1, 114 | lat: 22.246116, 115 | lng: 114.185732, 116 | guest_limit: 20, 117 | rules: Faker::StarWars.quote 118 | ) 119 | 120 | paris = Spot.create( 121 | description: 'gorgeous apartment right on the Champs-Elysees', 122 | image_url: 'https://s-ec.bstatic.com/images/hotel/max1024x768/258/25889444.jpg', 123 | price: 800, 124 | location: 'Paris, France', 125 | owner_id: 1, 126 | lat: 48.870086, 127 | lng: 2.306588, 128 | guest_limit: 6, 129 | rules: Faker::StarWars.quote 130 | ) 131 | 132 | miami = Spot.create( 133 | description: 'wonderful place right by the beach', 134 | image_url: 'https://cdn1.vox-cdn.com/uploads/chorus_asset/file/6569739/20160505_01_DSC_1862_HDR_LR.JPG', 135 | price: 1100, 136 | location: 'Miami, Florida, USA', 137 | owner_id: 1, 138 | lat: 25.785435, 139 | lng: -80.131368, 140 | guest_limit: 8, 141 | rules: Faker::ChuckNorris.fact 142 | ) 143 | 144 | barcelona = Spot.create( 145 | description: 'gorgeous home located near the Sagrada Familia', 146 | image_url: 'https://69b0d6debb47579c9a280043-costadeltennis.netdna-ssl.com/wp-content/uploads/2017/04/playafels-barcelona-hotel6.jpg', 147 | price: 800, 148 | location: 'Barcelona, Spain', 149 | owner_id: 1, 150 | lat: 41.377032, 151 | lng: 2.188995, 152 | guest_limit: 10, 153 | rules: Faker::Hipster.sentence 154 | ) 155 | 156 | milan = Spot.create( 157 | description: 'gorgeous house in the wealthiest Milanese district', 158 | image_url: 'https://www.milanmuseumguide.com/wp-content/uploads/Villa-Necchi-Campiglio-Milan-624x414.jpg', 159 | price: 1000, 160 | location: 'Milan, Italy', 161 | owner_id: 1, 162 | lat: 45.361689, 163 | lng: 9.162319, 164 | guest_limit: 6, 165 | rules: Faker::StarWars.quote 166 | ) 167 | 168 | vienna = Spot.create( 169 | description: 'apartment centrally located in Innere Stadt, Vienna', 170 | image_url: 'https://aff.bstatic.com/images/hotel/840x460/137/13767864.jpg', 171 | price: 900, 172 | location: 'Vienna, Austria', 173 | owner_id: 1, 174 | lat: 48.210068, 175 | lng: 16.378979, 176 | guest_limit: 4, 177 | rules: Faker::ChuckNorris.fact 178 | ) 179 | 180 | rio = Spot.create( 181 | description: 'wonderful beachside Leblon apartment', 182 | image_url: 'https://www.e-architect.co.uk/images/jpgs/sao_paulo/casa_6_m160310_1.jpg', 183 | price: 1300, 184 | location: 'Rio de Janeiro, Brazil', 185 | owner_id: 1, 186 | lat: -22.985907, 187 | lng: -43.224665, 188 | guest_limit: 15, 189 | rules: Faker::Hipster.sentence 190 | ) 191 | 192 | cape_town = Spot.create( 193 | description: 'amazing place in the suburbs of Cape Town', 194 | image_url: 'https://businesstech.co.za/news/wp-content/uploads/2015/10/R200-million-house.png', 195 | price: 1100, 196 | location: 'Cape Town, South Africa', 197 | owner_id: 1, 198 | lat: -34.032869, 199 | lng: 18.474536, 200 | guest_limit: 10, 201 | rules: Faker::StarWars.quote 202 | ) 203 | --------------------------------------------------------------------------------