├── log └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── photo_test.rb │ ├── user_test.rb │ ├── business_test.rb │ └── review_test.rb ├── system │ └── .keep ├── controllers │ ├── .keep │ ├── api │ │ ├── photos_controller_test.rb │ │ └── reviews_controller_test.rb │ └── businesses_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── photos.yml │ ├── reviews.yml │ ├── users.yml │ └── businesses.yml ├── integration │ └── .keep ├── application_system_test_case.rb └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── mag.png │ │ ├── default.jpg │ │ ├── favicon.ico │ │ ├── default_biz.png │ │ ├── logo_and_bar.png │ │ ├── one_star_review.png │ │ ├── transpBlack75.png │ │ ├── transpBlack90.png │ │ ├── two_star_review.png │ │ ├── welcome_image.png │ │ ├── evan-kirby-71617.jpg │ │ ├── five_star_review.png │ │ ├── footer_cityscape.png │ │ ├── four_star_review.png │ │ ├── neonbrand-231219.jpg │ │ ├── three_star_review.png │ │ ├── toa-heftiba-84799.jpg │ │ ├── arek-adeoye-221899.jpg │ │ ├── sebastian-lp-158732.jpg │ │ ├── sharon-chen-352895.jpg │ │ ├── austin-moncada-186841.jpg │ │ ├── david-nuescheler-140505.jpg │ │ ├── matthew-hamilton-160833.jpg │ │ ├── french.svg │ │ └── italy.svg │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── api │ │ │ ├── photos.coffee │ │ │ └── reviews.coffee │ │ ├── businesses.coffee │ │ ├── cable.js │ │ └── application.js │ ├── stylesheets │ │ ├── main.scss │ │ ├── api │ │ │ ├── photos.scss │ │ │ └── reviews.scss │ │ ├── map.scss │ │ ├── application.css │ │ ├── biz_landing_index.scss │ │ ├── css_reset.css │ │ ├── footer.scss │ │ ├── filter_search.scss │ │ ├── commments.scss │ │ ├── photo_show.scss │ │ ├── photo-index.scss │ │ ├── businesses.scss │ │ ├── navbar.scss │ │ ├── photo_upload.scss │ │ ├── session_form.scss │ │ ├── review_form.scss │ │ └── business_show.scss │ └── config │ │ └── manifest.js ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── photo.rb │ ├── review.rb │ ├── user.rb │ └── business.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── users_controller.rb │ │ ├── sessions_controller.rb │ │ ├── photos_controller.rb │ │ ├── reviews_controller.rb │ │ └── businesses_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ ├── photos │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── _photo.json.jbuilder │ │ ├── reviews │ │ │ ├── show.json.jbuilder │ │ │ ├── _review.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── users │ │ │ ├── show.json.jbuilder │ │ │ └── _user.json.jbuilder │ │ └── businesses │ │ │ ├── reviewers.json.jbuilder │ │ │ ├── _business.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── businesses_helper.rb │ ├── api │ │ ├── photos_helper.rb │ │ └── reviews_helper.rb │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── frontend ├── components │ ├── photos │ │ ├── photo_biz_show_index.jsx │ │ ├── photo_index_item.jsx │ │ ├── photos_container.jsx │ │ ├── photo_upload_container.jsx │ │ ├── photo_show_container.jsx │ │ ├── photo_show.jsx │ │ └── photo_upload.jsx │ ├── root.jsx │ ├── business │ │ ├── business_landing_container.jsx │ │ ├── biz_landing_index.jsx │ │ ├── biz_show_map.jsx │ │ ├── search_container.jsx │ │ ├── business_show_container.jsx │ │ ├── search.jsx │ │ ├── business_index.jsx │ │ ├── business_map.jsx │ │ └── biz_landing_index_item.jsx │ ├── NavBar │ │ ├── NavBar_container.jsx │ │ └── NavBar.jsx │ ├── search_form_container.jsx │ ├── landing │ │ ├── landing_container.jsx │ │ └── landing.jsx │ ├── reviews │ │ ├── review_index.jsx │ │ ├── reviews_container.jsx │ │ ├── review_form_container.jsx │ │ └── review_index_item.jsx │ ├── session │ │ └── session_form_container.jsx │ ├── App.jsx │ └── footer.jsx ├── util │ ├── user_api_util.js │ ├── session_api_util.js │ ├── business_api_util.js │ ├── review_api_util.js │ ├── photo_api_util.js │ ├── route_util.jsx │ ├── biz_show_marker_manager.js │ └── marker_manager.js ├── reducers │ ├── errors_reducer.js │ ├── page_reducer.js │ ├── session_reducer.js │ ├── filter_reducer.js │ ├── session_errors_reducer.js │ ├── business_reducer.js │ ├── user_reducer.js │ ├── photo_reducer.js │ ├── root_reducer.js │ └── review_reducer.js ├── actions │ ├── user_actions.js │ ├── filter_actions.js │ ├── business_actions.js │ ├── session_actions.js │ ├── photo_actions.js │ └── review_actions.js ├── store │ └── store.js └── entry.jsx ├── bin ├── bundle ├── rake ├── rails ├── yarn ├── spring ├── update └── setup ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ └── inflections.rb ├── cable.yml ├── routes.rb ├── locales │ └── en.yml ├── application.rb ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── puma.rb └── database.yml ├── config.ru ├── db ├── migrate │ ├── 20171026192246_businesses.rb │ ├── 20171029203202_remove_column_from_users.rb │ ├── 20171030184731_fix_column_name.rb │ ├── 20171030183525_add_column_to_businesses.rb │ ├── 20171101221825_add_columns_to_business.rb │ ├── 20171024025530_create_photos.rb │ ├── 20171029203759_add_attachment_image_to_users.rb │ ├── 20171029203836_add_attachment_image_to_photos.rb │ ├── 20171030182944_add_features.rb │ ├── 20171026190218_add_attachment_image_to_businesses.rb │ ├── 20171102183657_remove_columns_from_businesses.rb │ ├── 20171024025658_create_reviews.rb │ ├── 20171024013750_create_users.rb │ └── 20171024025355_create_businesses.rb └── schema.rb ├── Rakefile ├── .gitignore ├── webpack.config.js ├── package.json ├── Gemfile └── README.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /frontend/components/photos/photo_biz_show_index.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "font-awesome"; 3 | -------------------------------------------------------------------------------- /app/helpers/businesses_helper.rb: -------------------------------------------------------------------------------- 1 | module BusinessesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/photos_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::PhotosHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/reviews_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::ReviewsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/photos/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'photo', photo: @photo 2 | -------------------------------------------------------------------------------- /app/views/api/reviews/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'review', review: @review 2 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", user: @user 2 | -------------------------------------------------------------------------------- /app/assets/images/mag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/mag.png -------------------------------------------------------------------------------- /app/assets/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/default.jpg -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/default_biz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/default_biz.png -------------------------------------------------------------------------------- /app/assets/images/logo_and_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/logo_and_bar.png -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/one_star_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/one_star_review.png -------------------------------------------------------------------------------- /app/assets/images/transpBlack75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/transpBlack75.png -------------------------------------------------------------------------------- /app/assets/images/transpBlack90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/transpBlack90.png -------------------------------------------------------------------------------- /app/assets/images/two_star_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/two_star_review.png -------------------------------------------------------------------------------- /app/assets/images/welcome_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/welcome_image.png -------------------------------------------------------------------------------- /app/assets/images/evan-kirby-71617.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/evan-kirby-71617.jpg -------------------------------------------------------------------------------- /app/assets/images/five_star_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/five_star_review.png -------------------------------------------------------------------------------- /app/assets/images/footer_cityscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/footer_cityscape.png -------------------------------------------------------------------------------- /app/assets/images/four_star_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/four_star_review.png -------------------------------------------------------------------------------- /app/assets/images/neonbrand-231219.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/neonbrand-231219.jpg -------------------------------------------------------------------------------- /app/assets/images/three_star_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/three_star_review.png -------------------------------------------------------------------------------- /app/assets/images/toa-heftiba-84799.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/toa-heftiba-84799.jpg -------------------------------------------------------------------------------- /app/assets/images/arek-adeoye-221899.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/arek-adeoye-221899.jpg -------------------------------------------------------------------------------- /app/assets/images/sebastian-lp-158732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/sebastian-lp-158732.jpg -------------------------------------------------------------------------------- /app/assets/images/sharon-chen-352895.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/sharon-chen-352895.jpg -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | 2 | class StaticPagesController < ApplicationController 3 | def root 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/austin-moncada-186841.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/austin-moncada-186841.jpg -------------------------------------------------------------------------------- /app/assets/images/david-nuescheler-140505.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/david-nuescheler-140505.jpg -------------------------------------------------------------------------------- /app/assets/images/matthew-hamilton-160833.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreyladovsky/help/HEAD/app/assets/images/matthew-hamilton-160833.jpg -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /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/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /frontend/util/user_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchUser = userId => 2 | $.ajax({ 3 | method: "GET", 4 | url: `api/users/${userId}` 5 | }); 6 | -------------------------------------------------------------------------------- /app/views/api/photos/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @photos.each do |photo| 2 | json.set! photo.id do 3 | json.partial! "photo", photo: photo 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/reviews/_review.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! review, :id, :body, :user_id, :rating, :price_range, 2 | :noise_level, :delivery, :created_at 3 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /app/views/api/reviews/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @reviews.each do |review| 2 | json.set! review.id do 3 | json.partial! "review", review: review 4 | end 5 | 6 | end 7 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/models/photo_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PhotoTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :email, :image, :first_name, :last_name, :zip_code 2 | 3 | json.image asset_path(user.image.url(:thumb)) 4 | -------------------------------------------------------------------------------- /db/migrate/20171026192246_businesses.rb: -------------------------------------------------------------------------------- 1 | class Businesses < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :businesses, :profile_pic 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/business_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BusinessTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/review_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ReviewTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20171029203202_remove_column_from_users.rb: -------------------------------------------------------------------------------- 1 | class RemoveColumnFromUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :users, :profile_pic 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171030184731_fix_column_name.rb: -------------------------------------------------------------------------------- 1 | class FixColumnName < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :businesses, :rating_count, :review_count 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/businesses/reviewers.json.jbuilder: -------------------------------------------------------------------------------- 1 | @users.each do |user| 2 | json.set! user.id do 3 | json.extract! user, :id, :email, :image, :first_name, :last_name, :zip_code 4 | 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20171030183525_add_column_to_businesses.rb: -------------------------------------------------------------------------------- 1 | class AddColumnToBusinesses < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :businesses, :rating_count, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: Help_production 11 | -------------------------------------------------------------------------------- /test/controllers/api/photos_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::PhotosControllerTest < ActionDispatch::IntegrationTest 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 < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/photos/_photo.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! photo, :id, :user_id, :business_id, :image 2 | 3 | json.image asset_path(photo.image.url(:croppable)) 4 | json.image_medium asset_path(photo.image.url(:croppable)) 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/photos.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/photos controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /test/fixtures/photos.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user_id: 1 5 | business_id: 1 6 | 7 | two: 8 | user_id: 1 9 | business_id: 1 10 | -------------------------------------------------------------------------------- /db/migrate/20171101221825_add_columns_to_business.rb: -------------------------------------------------------------------------------- 1 | class AddColumnsToBusiness < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :businesses, :lat, :float 4 | add_column :businesses, :lng, :float 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 | -------------------------------------------------------------------------------- /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_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/photos.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/businesses.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 | -------------------------------------------------------------------------------- /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/reducers/errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import SessionErrorsReducer from "./session_errors_reducer"; 3 | 4 | const ErrorsReducer = combineReducers({ 5 | session: SessionErrorsReducer 6 | }); 7 | 8 | export default ErrorsReducer; 9 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /db/migrate/20171024025530_create_photos.rb: -------------------------------------------------------------------------------- 1 | class CreatePhotos < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :photos do |t| 4 | t.integer :user_id, null: false 5 | t.integer :business_id, null: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /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 | .review-items-ul { 5 | display: flex; 6 | flex-direction: column-reverse; 7 | } 8 | -------------------------------------------------------------------------------- /app/models/photo.rb: -------------------------------------------------------------------------------- 1 | class Photo < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :business 4 | 5 | 6 | has_attached_file :image, styles: { thumb: '100x100', croppable: '600x600>', big: '1000x1000>' } 7 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |
your react broke (check console)
10 | -------------------------------------------------------------------------------- /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', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20171029203759_add_attachment_image_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToUsers < ActiveRecord::Migration[5.1] 2 | def self.up 3 | change_table :users do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :users, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171029203836_add_attachment_image_to_photos.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToPhotos < ActiveRecord::Migration[5.1] 2 | def self.up 3 | change_table :photos do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :photos, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171030182944_add_features.rb: -------------------------------------------------------------------------------- 1 | class AddFeatures < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :businesses, :rating, :integer 4 | add_column :businesses, :price_range, :integer 5 | add_column :businesses, :delivery, :boolean 6 | add_column :businesses, :noise_level, :integer 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20171026190218_add_attachment_image_to_businesses.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToBusinesses < ActiveRecord::Migration[5.1] 2 | def self.up 3 | change_table :businesses do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :businesses, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../config/environment', __FILE__) 2 | require 'rails/test_help' 3 | 4 | class ActiveSupport::TestCase 5 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 6 | fixtures :all 7 | 8 | # Add more helper methods to be used by all tests here... 9 | end 10 | -------------------------------------------------------------------------------- /app/views/api/businesses/_business.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! business, :id, :name, :address, :phone_number, 2 | :cuisine, :website, :image, :average_rating, :review_count, :lat, :lng, 3 | :price_range, :noise_level, :delivery 4 | 5 | json.image asset_path(business.image.url(:thumb)) 6 | json.image_medium asset_path(business.image.url(:croppable)) 7 | -------------------------------------------------------------------------------- /frontend/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | import * as UserAPIUtil from "../util/user_api_util"; 2 | 3 | export const RECEIVE_USER = "RECEIVE_USER"; 4 | 5 | export const receiveUser = user => ({ 6 | type: RECEIVE_USER, 7 | user 8 | }); 9 | 10 | export const fetchUser = userId => dispatch => 11 | UserAPIUtil.fetchUser(userId).then(user => dispatch(receiveUser(user))); 12 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { HashRouter } from "react-router-dom"; 4 | import App from "./App"; 5 | 6 | const Root = ({ store }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default Root; 15 | -------------------------------------------------------------------------------- /app/views/api/businesses/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @businesses.each do |business| 5 | json.set! business.id do 6 | json.partial! "business", business: business 7 | json.reviewIds business.reviews.map(&:id) 8 | json.reviews business.reviews 9 | json.reviewers business.reviewers 10 | json.photoIds business.photos.map(&:id) 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map.scss: -------------------------------------------------------------------------------- 1 | #map-container { 2 | width: 300px; 3 | height: 350px; 4 | position: sticky !important; 5 | top: 0px !important; 6 | margin-bottom: 40px; 7 | } 8 | 9 | 10 | .map-and-index-ul { 11 | display: flex; 12 | justify-content: center; 13 | margin-top: 70px; 14 | } 15 | 16 | .biz-map-index-page { 17 | margin-top: 80px; 18 | margin-left: 80px; 19 | } 20 | -------------------------------------------------------------------------------- /db/migrate/20171102183657_remove_columns_from_businesses.rb: -------------------------------------------------------------------------------- 1 | class RemoveColumnsFromBusinesses < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :businesses, :rating 4 | remove_column :businesses, :review_count 5 | remove_column :businesses, :price_range 6 | remove_column :businesses, :delivery 7 | remove_column :businesses, :noise_level 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/review.rb: -------------------------------------------------------------------------------- 1 | class Review < ApplicationRecord 2 | validates :rating, :body, :user_id, :business_id, presence: true 3 | validates_uniqueness_of :user_id, scope: :business_id 4 | belongs_to :user 5 | belongs_to :business 6 | 7 | def rating_must_be_greater_than_zero 8 | if rating.present? && rating < 1 9 | errors.add(:rating, "must be greater than zero") 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signup = user => 2 | $.ajax({ 3 | method: "post", 4 | url: "api/users", 5 | data: { user } 6 | }); 7 | 8 | export const login = user => 9 | $.ajax({ 10 | method: "post", 11 | url: "api/session", 12 | data: { user } 13 | }); 14 | 15 | export const logout = () => 16 | $.ajax({ 17 | method: "delete", 18 | url: "api/session" 19 | }); 20 | -------------------------------------------------------------------------------- /test/fixtures/reviews.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user_id: 1 5 | business_id: 1 6 | rating: 1 7 | price_range: 1 8 | body: MyText 9 | deliver: false 10 | noise_level: 1 11 | 12 | two: 13 | user_id: 1 14 | business_id: 1 15 | rating: 1 16 | price_range: 1 17 | body: MyText 18 | deliver: false 19 | noise_level: 1 20 | -------------------------------------------------------------------------------- /frontend/reducers/page_reducer.js: -------------------------------------------------------------------------------- 1 | import { NEXT_PAGE, CLEAR_PAGE } from "../actions/session_actions"; 2 | 3 | const PageReducer = (oldState = null, action) => { 4 | Object.freeze(oldState); 5 | switch (action.type) { 6 | case NEXT_PAGE: 7 | return action.page; 8 | case CLEAR_PAGE: 9 | return null; 10 | default: 11 | return oldState; 12 | } 13 | }; 14 | 15 | export default PageReducer; 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import logger from "redux-logger"; 3 | import thunk from "redux-thunk"; 4 | import RootReducer from "../reducers/root_reducer"; 5 | import SessionReducer from "../reducers/session_reducer"; 6 | 7 | const configureStore = (preloadedState = {}) => 8 | createStore(RootReducer, preloadedState, applyMiddleware(thunk)); 9 | 10 | export default configureStore; 11 | -------------------------------------------------------------------------------- /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/20171024025658_create_reviews.rb: -------------------------------------------------------------------------------- 1 | class CreateReviews < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :reviews do |t| 4 | t.integer :user_id, null: false 5 | t.integer :business_id, null: false 6 | t.integer :rating, null: false 7 | t.integer :price_range 8 | t.text :body, null: false 9 | t.boolean :delivery 10 | t.integer :noise_level 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from "lodash/merge"; 2 | import { RECEIVE_CURRENT_USER } from "../actions/session_actions"; 3 | 4 | const SessionReducer = (oldState = { currentUser: null }, action) => { 5 | Object.freeze(oldState); 6 | switch (action.type) { 7 | case RECEIVE_CURRENT_USER: 8 | return { currentUser: action.currentUser }; 9 | default: 10 | return oldState; 11 | } 12 | }; 13 | 14 | export default SessionReducer; 15 | -------------------------------------------------------------------------------- /frontend/actions/filter_actions.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_BOUNDS = "UPDATE_BOUNDS"; 2 | export const FRONT_FILTER = "FRONT_FILTER"; 3 | export const CLEAR_FILTER = "CLEAR_FILTER"; 4 | 5 | export const updateBounds = (bounds, value) => ({ 6 | type: UPDATE_BOUNDS, 7 | bounds, 8 | value 9 | }); 10 | 11 | export const frontFilter = bounds => ({ 12 | type: FRONT_FILTER, 13 | bounds 14 | }); 15 | 16 | 17 | export const clearFilter = () => ({ 18 | type: CLEAR_FILTER 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/reducers/filter_reducer.js: -------------------------------------------------------------------------------- 1 | import { FRONT_FILTER, CLEAR_FILTER } from "../actions/filter_actions"; 2 | import merge from "lodash/merge"; 3 | 4 | const FilterReducer = (oldState = null, action) => { 5 | Object.freeze(oldState); 6 | switch (action.type) { 7 | case FRONT_FILTER: 8 | return merge({}, oldState, action.bounds); 9 | case CLEAR_FILTER: 10 | return {}; 11 | default: 12 | return oldState; 13 | } 14 | }; 15 | 16 | export default FilterReducer; 17 | -------------------------------------------------------------------------------- /db/migrate/20171024013750_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name, null: false 5 | t.string :last_name, null: false 6 | t.string :email, null: false 7 | t.string :password_digest, null: false 8 | t.string :session_token, null: false 9 | t.string :zip_code, null: false 10 | t.date :birthday 11 | t.string :profile_pic 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /frontend/reducers/session_errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_CURRENT_USER, 3 | RECEIVE_ERRORS, 4 | CLEAR_ERRORS 5 | } from "../actions/session_actions"; 6 | import merge from "lodash/merge"; 7 | 8 | const SessionErrorsReducer = (oldState = [], action) => { 9 | switch (action.type) { 10 | case RECEIVE_CURRENT_USER: 11 | return []; 12 | case RECEIVE_ERRORS: 13 | return action.errors; 14 | case CLEAR_ERRORS: 15 | return []; 16 | default: 17 | return oldState; 18 | } 19 | }; 20 | 21 | export default SessionErrorsReducer; 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] 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/reducers/business_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_BUSINESSES, 3 | RECEIVE_BUSINESS 4 | } from "../actions/business_actions"; 5 | import merge from "lodash/merge"; 6 | 7 | const BusinessReducer = (oldState = {}, action) => { 8 | Object.freeze(oldState); 9 | switch (action.type) { 10 | case RECEIVE_BUSINESSES: 11 | return action.businesses; 12 | case RECEIVE_BUSINESS: 13 | return merge({}, oldState, { [action.business.id]: action.business }); 14 | default: 15 | return oldState; 16 | } 17 | }; 18 | 19 | export default BusinessReducer; 20 | -------------------------------------------------------------------------------- /frontend/reducers/user_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_USER } from "../actions/user_actions"; 2 | import { RECEIVE_REVIEWERS } from "../actions/business_actions"; 3 | import merge from "lodash/merge"; 4 | 5 | const UserReducer = (oldState = {}, action) => { 6 | Object.freeze(oldState); 7 | switch (action.type) { 8 | case RECEIVE_REVIEWERS: 9 | return action.reviewers; 10 | case RECEIVE_USER: 11 | return merge({}, oldState, { [action.user.id]: action.user }); 12 | default: 13 | return oldState; 14 | } 15 | }; 16 | 17 | export default UserReducer; 18 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | first_name: MyString 5 | last_name: MyString 6 | email: MyString 7 | password_digest: MyString 8 | session_token: MyString 9 | zip_code: MyString 10 | birthday: 2017-10-23 11 | profile_pic: MyString 12 | 13 | two: 14 | first_name: MyString 15 | last_name: MyString 16 | email: MyString 17 | password_digest: MyString 18 | session_token: MyString 19 | zip_code: MyString 20 | birthday: 2017-10-23 21 | profile_pic: MyString 22 | -------------------------------------------------------------------------------- /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 "api/users/show" 8 | else 9 | render json: @user.errors.full_messages, status: 404 10 | end 11 | end 12 | 13 | def show 14 | @user = User.find(params[:id]); 15 | end 16 | 17 | private 18 | 19 | def user_params 20 | params.require(:user).permit(:email, :password, :first_name, :last_name, :zip_code, :birthday, :image) 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /frontend/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import configureStore from "./store/store"; 4 | import Root from "./components/root"; 5 | 6 | document.addEventListener("DOMContentLoaded", () => { 7 | let store; 8 | if (window.currentUser) { 9 | const preloadedState = { session: { currentUser: window.currentUser } }; 10 | store = configureStore(preloadedState); 11 | delete window.currentUser; 12 | } else { 13 | store = configureStore(); 14 | } 15 | 16 | const root = document.getElementById("root"); 17 | ReactDOM.render(, root); 18 | }); 19 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | 2 | Rails.application.routes.draw do 3 | get 'businesses/index' 4 | 5 | 6 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 7 | root to: 'static_pages#root' 8 | 9 | namespace :api, defaults: { format: :json } do 10 | resources :users, only: [:create, :show] 11 | resource :session, only: [:new, :create, :destroy] 12 | resources :businesses do 13 | resources :reviews, except: [:show, :destroy] 14 | get "reviewers", on: :member 15 | end 16 | resources :photos 17 | resources :reviews, only: [:show, :destroy] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials( 5 | params[:user][:email], 6 | params[:user][:password] 7 | ) 8 | 9 | if @user 10 | log_in(@user) 11 | render 'api/users/show' 12 | else 13 | render json: ['Invalid credentials'], status: 404 14 | end 15 | end 16 | 17 | def destroy 18 | @user = current_user 19 | if @user 20 | logout 21 | render json: {} 22 | else 23 | render json: ["Nobody currently logged in"], status: 404 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /frontend/components/business/business_landing_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import BizLandingIndex from "./biz_landing_index"; 3 | import { fetchBusinesses } from "../../actions/business_actions"; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | business: Object.values(state.business), 8 | currentUser: state.session.currentUser 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch, ownProps) => { 13 | return { 14 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)) 15 | }; 16 | }; 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(BizLandingIndex); 19 | -------------------------------------------------------------------------------- /app/views/api/businesses/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'business', business: @business 2 | 3 | json.extract! @business, :mon_start_time, :mon_end_time, :tue_start_time, :tue_end_time, :wed_start_time, 4 | :wed_end_time, :thur_start_time, :thur_end_time, :fri_start_time, :fri_end_time, 5 | :sat_start_time, :sat_end_time, :sun_start_time, :sun_end_time, :lat, :lng 6 | 7 | json.reviews do 8 | @business.reviews.each do |review| 9 | json.set! review.id do 10 | json.partial! "api/reviews/review", review: review 11 | end 12 | end 13 | end 14 | 15 | json.photos do 16 | json.array! @business.photos, partial: 'api/photos/photo', as: :photo 17 | end 18 | -------------------------------------------------------------------------------- /frontend/util/business_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchBusinesses = data => { 2 | return $.ajax({ 3 | method: "GET", 4 | url: "api/businesses", 5 | data: { business: data } 6 | }); 7 | }; 8 | 9 | export const fetchBusiness = businessId => 10 | $.ajax({ 11 | method: "GET", 12 | url: `api/businesses/${businessId}` 13 | }); 14 | 15 | export const createBusiness = data => 16 | $.ajax({ 17 | method: "POST", 18 | url: "api/business", 19 | data: { data } 20 | }); 21 | 22 | export const fetchReviewers = businessId => 23 | $.ajax({ 24 | method: "GET", 25 | url: "api/businesses/" + businessId + "/reviewers" 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/reducers/photo_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_PHOTOS, RECEIVE_PHOTO } from "../actions/photo_actions"; 2 | import { RECEIVE_BUSINESS } from "../actions/business_actions"; 3 | import merge from "lodash/merge"; 4 | 5 | const PhotoReducer = (oldState = {}, action) => { 6 | Object.freeze(oldState); 7 | switch (action.type) { 8 | case RECEIVE_PHOTOS: 9 | return action.photos; 10 | case RECEIVE_PHOTO: 11 | return merge({}, oldState, { [action.photo.id]: action.photo }); 12 | case RECEIVE_BUSINESS: 13 | return action.business.photos || {}; 14 | default: 15 | return oldState; 16 | } 17 | }; 18 | 19 | export default PhotoReducer; 20 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavBar_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import NavBar from "./NavBar"; 3 | import { logout, clearPage, nextPage } from "../../actions/session_actions"; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | currentUser: state.session.currentUser, 8 | intendedPage: state.intendedPage 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch, ownProps) => { 13 | return { 14 | logout: () => dispatch(logout()), 15 | clearPage: () => dispatch(clearPage()), 16 | nextPage: page => dispatch(nextPage(page)) 17 | }; 18 | }; 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(NavBar); 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 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /frontend/components/search_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchBusinesses } from "../actions/business_actions"; 3 | import { frontFilter, clearFilter } from "../actions/filter_actions"; 4 | import SearchForm from "./search_form"; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | return {}; 8 | }; 9 | 10 | const mapDispatchToProps = (dispatch, ownProps) => { 11 | return { 12 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)), 13 | frontFilter: bounds => dispatch(frontFilter(bounds)), 14 | clearFilter: () => dispatch(clearFilter()) 15 | }; 16 | }; 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(SearchForm); 19 | -------------------------------------------------------------------------------- /frontend/components/business/biz_landing_index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BizLandingIndexItem from "./biz_landing_index_item"; 3 | import { shuffle } from "underscore"; 4 | 5 | class BizLandingIndex extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const businesses = this.props.business.slice(0, 6).map(business => { 12 | return ; 13 | }); 14 | return ( 15 |
16 |
    {businesses}
17 |
18 | ); 19 | } 20 | } 21 | 22 | export default BizLandingIndex; 23 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | helper_method :logged_in?, :current_user 4 | 5 | def logged_in? 6 | !!current_user 7 | end 8 | 9 | def log_in(user) 10 | @current_user = user 11 | session[:session_token] = @current_user.reset_session_token! 12 | end 13 | 14 | def current_user 15 | @current_user ||= User.find_by(session_token: session[:session_token]) 16 | end 17 | 18 | def logout 19 | current_user.reset_session_token! 20 | session[:session_token] = nil 21 | end 22 | 23 | def require_login 24 | redirect_to new_session_url unless logged_in? 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /.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 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | /node_modules 17 | /yarn-error.log 18 | 19 | .byebug_history 20 | node_modules/ 21 | bundle.js 22 | bundle.js.map 23 | .byebug_history 24 | .DS_Store 25 | npm-debug.log 26 | 27 | # Ignore application configuration 28 | /config/application.yml 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/photos/photo_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class PhotoIndexItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.photoLink = this.photoLink.bind(this); 8 | } 9 | 10 | photoLink() { 11 | this.props.history.push(this.props.path + "/" + this.props.photo.id); 12 | } 13 | 14 | render() { 15 | if (this.props.photo === undefined) { 16 | return null; 17 | } else { 18 | return ( 19 |
  • 20 | 21 |
  • 22 | ); 23 | } 24 | } 25 | } 26 | 27 | export default PhotoIndexItem; 28 | -------------------------------------------------------------------------------- /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, or any plugin's 5 | // 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. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require jquery 15 | //= require jquery_ujs 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import SessionReducer from "./session_reducer"; 3 | import ErrorsReducer from "./errors_reducer"; 4 | import BusinessReducer from "./business_reducer"; 5 | import PageReducer from "./page_reducer"; 6 | import ReviewReducer from "./review_reducer"; 7 | import UserReducer from "./user_reducer"; 8 | import PhotoReducer from "./photo_reducer"; 9 | import FilterReducer from "./filter_reducer"; 10 | 11 | const RootReducer = combineReducers({ 12 | session: SessionReducer, 13 | errors: ErrorsReducer, 14 | business: BusinessReducer, 15 | intendedPage: PageReducer, 16 | reviews: ReviewReducer, 17 | reviewers: UserReducer, 18 | photos: PhotoReducer, 19 | filters: FilterReducer 20 | }); 21 | 22 | export default RootReducer; 23 | -------------------------------------------------------------------------------- /app/controllers/api/photos_controller.rb: -------------------------------------------------------------------------------- 1 | 2 | class Api::PhotosController < ApplicationController 3 | def index 4 | @photos = Photo.all 5 | end 6 | 7 | def show 8 | @photo = Photo.find(params[:id]) 9 | end 10 | 11 | def create 12 | @photo = Photo.new(photo_params) 13 | if @photo.save 14 | business = @photo.business 15 | render json: business, include: [:photos] 16 | else 17 | render json: @photo.errors.full_messages, status: 422 18 | end 19 | end 20 | 21 | def destroy 22 | @photo = Photo.find(params[:id]) 23 | business = @photo.business 24 | @photo.destroy! 25 | render json: business, include: [:photos] 26 | end 27 | 28 | private 29 | def photo_params 30 | params.require(:photo).permit(:image, :business_id, :user_id) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /frontend/util/review_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchReviews = businessId => 2 | $.ajax({ 3 | method: "GET", 4 | url: `api/businesses/${businessId}/reviews` 5 | }); 6 | 7 | export const fetchReview = reviewId => 8 | $.ajax({ 9 | method: "GET", 10 | url: `api/reviews/${reviewId}` 11 | }); 12 | 13 | export const createReview = review => 14 | $.ajax({ 15 | method: "POST", 16 | url: `api/businesses/${review.business_id}/reviews`, 17 | data: { review } 18 | }); 19 | 20 | export const updateReview = review => 21 | $.ajax({ 22 | method: "PATCH", 23 | url: `api/businesses/${review.business_id}/reviews/${review.id}`, 24 | data: { review } 25 | }); 26 | 27 | export const deleteReview = reviewId => 28 | $.ajax({ 29 | method: "DELETE", 30 | url: `api/reviews/${reviewId}` 31 | }); 32 | -------------------------------------------------------------------------------- /frontend/util/photo_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchPhoto = photoId => 2 | $.ajax({ 3 | method: "GET", 4 | url: `api/photos/${photoId}` 5 | }); 6 | 7 | export const fetchBizPhotos = businessId => 8 | $.ajax({ 9 | method: "GET", 10 | url: `api/businesses/${businessId}/photos` 11 | }); 12 | 13 | export const deletePhoto = photoId => 14 | $.ajax({ 15 | method: "DELETE", 16 | url: `api/photos/${photoId}` 17 | }); 18 | 19 | export const updatePhoto = photo => 20 | $.ajax({ 21 | method: "PATCH", 22 | url: `api/photos/${photo.id}`, 23 | data: { photo } 24 | }); 25 | 26 | export const createPhoto = formData => 27 | $.ajax({ 28 | method: "POST", 29 | url: "api/photos", 30 | dataType: "json", 31 | contentType: false, 32 | processData: false, 33 | data: formData 34 | }); 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * This is a manifest file that'll be compiled into application.css, which will include all the files 4 | * listed below. 5 | * 6 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 7 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 8 | * 9 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 10 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 11 | * files in this directory. Styles in this file should be added after the last require_* statement. 12 | * It is generally better to create a new file per style scope. 13 | * 14 | *= require_tree . 15 | *= require_self 16 | *= main 17 | *= require font-awesome 18 | */ 19 | -------------------------------------------------------------------------------- /test/controllers/businesses_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BusinessesControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get businesses_index_url 6 | assert_response :success 7 | end 8 | 9 | test "should get show" do 10 | get businesses_show_url 11 | assert_response :success 12 | end 13 | 14 | test "should get create" do 15 | get businesses_create_url 16 | assert_response :success 17 | end 18 | 19 | test "should get update" do 20 | get businesses_update_url 21 | assert_response :success 22 | end 23 | 24 | test "should get destroy" do 25 | get businesses_destroy_url 26 | assert_response :success 27 | end 28 | 29 | test "should get new" do 30 | get businesses_new_url 31 | assert_response :success 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | context: __dirname, 6 | entry: "./frontend/entry.jsx", 7 | output: { 8 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 9 | filename: "bundle.js" 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: [/\.jsx?$/, /\.js?$/], 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | query: { 18 | presets: ['es2015', 'react'] 19 | } 20 | } 21 | ] 22 | }, 23 | devtool: 'source-map', 24 | resolve: { 25 | extensions: [".js", ".jsx", "*"] 26 | }, 27 | 28 | 29 | plugins: [ 30 | new webpack.DefinePlugin({ 31 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 32 | }), 33 | ], 34 | 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/reducers/review_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_REVIEWS, 3 | RECEIVE_REVIEW, 4 | REMOVE_REVIEW 5 | } from "../actions/review_actions"; 6 | import { RECEIVE_BUSINESS } from "../actions/business_actions"; 7 | import merge from "lodash/merge"; 8 | 9 | const ReviewReducer = (oldState = {}, action) => { 10 | Object.freeze(oldState); 11 | switch (action.type) { 12 | case RECEIVE_REVIEWS: 13 | return action.reviews; 14 | case RECEIVE_REVIEW: 15 | return merge({}, oldState, { [action.review.id]: action.review }); 16 | case REMOVE_REVIEW: 17 | let newState = merge({}, oldState); 18 | delete newState[action.reviewId]; 19 | return newState; 20 | case RECEIVE_BUSINESS: 21 | return action.business.reviews || {}; 22 | default: 23 | return oldState; 24 | } 25 | }; 26 | 27 | export default ReviewReducer; 28 | -------------------------------------------------------------------------------- /frontend/components/landing/landing_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { logout, nextPage, clearPage } from "../../actions/session_actions"; 3 | import Landing from "./landing"; 4 | import { fetchBusinesses } from "../../actions/business_actions"; 5 | import { updateBounds } from "../../actions/filter_actions"; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | currentUser: state.session.currentUser, 10 | intendedPage: state.intendedPage 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = dispatch => { 15 | return { 16 | logout: () => dispatch(logout()), 17 | nextPage: page => dispatch(nextPage(page)), 18 | clearPage: () => dispatch(clearPage()), 19 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)) 20 | }; 21 | }; 22 | 23 | export default connect(mapStateToProps, mapDispatchToProps)(Landing); 24 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /frontend/components/business/biz_show_map.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import ReactDom from "react-dom"; 4 | import MarkerManager from "../../util/biz_show_marker_manager"; 5 | 6 | class BizShowMap extends React.Component { 7 | componentDidMount() { 8 | const mapOptions = { 9 | center: { lat: this.props.business.lat, lng: this.props.business.lng }, 10 | zoom: 15 11 | }; 12 | 13 | this.mapNode = document.getElementById("map-container"); 14 | this.map = new google.maps.Map(this.mapNode, mapOptions); 15 | this.MarkerManager = new MarkerManager(this.map); 16 | } 17 | 18 | componentDidUpdate() { 19 | this.MarkerManager.updateMarkers(this.props.business); 20 | } 21 | 22 | render() { 23 | return
    (this.mapNode = map)} />; 24 | } 25 | } 26 | 27 | export default withRouter(BizShowMap); 28 | -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Route, Redirect, withRouter } from "react-router-dom"; 4 | 5 | const Auth = ({ component: Component, path, loggedIn }) => ( 6 | 9 | !loggedIn ? : 10 | } 11 | /> 12 | ); 13 | const Protected = ({ component: Component, path, loggedIn }) => ( 14 | 17 | loggedIn ? : 18 | } 19 | /> 20 | ); 21 | 22 | const mapStateToProps = state => { 23 | return { loggedIn: Boolean(state.session.currentUser) }; 24 | }; 25 | 26 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth)); 27 | export const ProtectedRoute = withRouter( 28 | connect(mapStateToProps, null)(Protected) 29 | ); 30 | -------------------------------------------------------------------------------- /db/migrate/20171024025355_create_businesses.rb: -------------------------------------------------------------------------------- 1 | class CreateBusinesses < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :businesses do |t| 4 | t.string :name, null: false 5 | t.string :address, null: false 6 | t.string :phone_number, null: false 7 | t.string :profile_pic, null: false 8 | t.string :cuisine, null: false 9 | t.string :website 10 | t.integer :mon_start_time 11 | t.integer :mon_end_time 12 | t.integer :tue_start_time 13 | t.integer :tue_end_time 14 | t.integer :wed_start_time 15 | t.integer :wed_end_time 16 | t.integer :thur_start_time 17 | t.integer :thur_end_time 18 | t.integer :fri_start_time 19 | t.integer :fri_end_time 20 | t.integer :sat_start_time 21 | t.integer :sat_end_time 22 | t.integer :sun_start_time 23 | t.integer :sun_end_time 24 | 25 | t.timestamps 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/biz_landing_index.scss: -------------------------------------------------------------------------------- 1 | .biz-landing-index-container { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .biz-landing-index-ul { 7 | display: flex; 8 | flex-wrap: wrap; 9 | max-width: 985px; 10 | } 11 | 12 | 13 | .biz-landing-li-item { 14 | width: 300px; 15 | height: 320px; 16 | border: 1px solid #CCCCCC; 17 | border-radius: 4px; 18 | margin: 12px; 19 | } 20 | 21 | 22 | .biz-landing-photo { 23 | width: 100%; 24 | height: 184px; 25 | object-fit: cover; 26 | border-radius: 4px 4px 0px 0px; 27 | margin-bottom: 15px; 28 | } 29 | 30 | .biz-landing-index-name { 31 | font-size: 16px; 32 | font-weight: bold; 33 | font-family: sans-serif; 34 | color: #0073BB; 35 | margin-left: 15px; 36 | } 37 | 38 | .biz-landing-info { 39 | font-family: sans-serif; 40 | font-size: 14px; 41 | margin-left: 16px; 42 | margin-top: 5px; 43 | } 44 | 45 | .address-landing { 46 | margin-top: 4px; 47 | } 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/css_reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | CSS Reset taken from App Academy Github 3 | */ 4 | html, body, header, nav, h1, a, 5 | ul, li, strong, main, button, i, 6 | section, img, div, h2, p, form, 7 | fieldset, label, input, textarea, 8 | span, article, footer, time, small { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | outline: 0; 13 | font: inherit; 14 | color: inherit; 15 | text-align: inherit; 16 | text-decoration: inherit; 17 | vertical-align: inherit; 18 | box-sizing: inherit; 19 | background: transparent; 20 | } 21 | 22 | ul { 23 | list-style: none; 24 | } 25 | 26 | img { 27 | display: block; 28 | width: 100%; 29 | height: auto; 30 | } 31 | 32 | input[type="password"], 33 | input[type="email"], 34 | input[type="text"], 35 | input[type="submit"], 36 | textarea, 37 | button { 38 | 39 | -webkit-appearance: none; 40 | -moz-appearance: none; 41 | } 42 | 43 | button, 44 | input[type="submit"] { 45 | cursor: pointer; 46 | } 47 | -------------------------------------------------------------------------------- /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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/assets/stylesheets/footer.scss: -------------------------------------------------------------------------------- 1 | .footer-pic { 2 | background-image: image-url('footer_cityscape.png'); 3 | height: 150px; 4 | width: 653px; 5 | background-repeat: no-repeat; 6 | margin: 0 auto; 7 | } 8 | 9 | .footer { 10 | border-top: 1px solid #CCCCCC; 11 | display: flex; 12 | flex-direction: column; 13 | 14 | } 15 | 16 | .all-landing-ul { 17 | font-family: sans-serif; 18 | display: flex; 19 | justify-content: space-between; 20 | margin: 0 auto; 21 | margin-top: 50px; 22 | max-width: 1000px; 23 | 24 | } 25 | .landing-ul-header { 26 | color: #D70306; 27 | font-weight: bold; 28 | font-style: 18px; 29 | 30 | } 31 | 32 | .all-landing-container { 33 | display: flex; 34 | justify-content: space-between; 35 | } 36 | 37 | .info-landing-ul { 38 | display: flex; 39 | flex-direction: column; 40 | margin-top: 10px; 41 | } 42 | 43 | .info-landing-ul a { 44 | margin-top: 10px; 45 | font-size: 14px; 46 | color: #0F78C9; 47 | } 48 | 49 | .info-landing-ul a:hover { 50 | text-decoration: underline; 51 | } 52 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 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 Help 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 5.1 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | config.paperclip_defaults = { 19 | storage: :s3, 20 | s3_protocol: :https, 21 | s3_credentials: { 22 | bucket: ENV["s3_bucket"], 23 | access_key_id: ENV["s3_access_key_id"], 24 | secret_access_key: ENV["s3_secret_access_key"], 25 | s3_region: ENV["s3_region"], 26 | s3_host_name: "s3.amazonaws.com", 27 | 28 | } 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /frontend/components/reviews/review_index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NavBar from "../NavBar/NavBar"; 3 | import ReviewIndexItem from "./review_index_item"; 4 | 5 | class ReviewIndex extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | if (this.props.reviews === undefined) { 12 | return null; 13 | } else { 14 | const reviews = this.props.reviews.map(review => { 15 | return ( 16 | 25 | ); 26 | }); 27 | 28 | return ( 29 |
    30 |
      {reviews}
    31 |
    32 | ); 33 | } 34 | } 35 | } 36 | 37 | export default ReviewIndex; 38 | -------------------------------------------------------------------------------- /test/fixtures/businesses.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | address: MyString 6 | phone_number: MyString 7 | profile_pic: MyString 8 | cuisine: MyString 9 | website: MyString 10 | mon_start_time: 1 11 | mon_end_time: 1 12 | tue_start_time: 1 13 | tue_end_time: 1 14 | wed_start_time: 1 15 | wed_end_time: 1 16 | thur_start_time: 1 17 | thur_end_time: 1 18 | fri_start_time: 1 19 | fri_end_time: 1 20 | sat_start_time: 1 21 | sat_end_time: 1 22 | sun_start_time: 1 23 | sun_end_time: 1 24 | 25 | two: 26 | name: MyString 27 | address: MyString 28 | phone_number: MyString 29 | profile_pic: MyString 30 | cuisine: MyString 31 | website: MyString 32 | mon_start_time: 1 33 | mon_end_time: 1 34 | tue_start_time: 1 35 | tue_end_time: 1 36 | wed_start_time: 1 37 | wed_end_time: 1 38 | thur_start_time: 1 39 | thur_end_time: 1 40 | fri_start_time: 1 41 | fri_end_time: 1 42 | sat_start_time: 1 43 | sat_end_time: 1 44 | sun_start_time: 1 45 | sun_end_time: 1 46 | -------------------------------------------------------------------------------- /frontend/util/biz_show_marker_manager.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | export default class MarkerManager { 4 | constructor(map) { 5 | this.map = map; 6 | this.markers = {}; 7 | } 8 | 9 | updateMarkers(business) { 10 | this.createMarkerFromBusiness(business); 11 | } 12 | 13 | createMarkerFromBusiness(business) { 14 | const position = new google.maps.LatLng(business.lat, business.lng); 15 | const marker = new google.maps.Marker({ 16 | content: `${business.image}, ${business.name}, ${business.address}`, 17 | position, 18 | map: this.map, 19 | businessId: business.id, 20 | animation: google.maps.Animation.DROP 21 | }); 22 | let img = business.image; 23 | let infowindow = new google.maps.InfoWindow({ 24 | content: 25 | ` ` + 26 | business.name + 27 | " " + 28 | business.address 29 | }); 30 | 31 | marker.addListener("click", function() { 32 | infowindow.open(marker.get("map"), marker); 33 | }); 34 | 35 | this.markers[marker.businessId] = marker; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/components/photos/photos_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchPhoto, fetchBizPhotos } from "../../actions/photo_actions"; 3 | import { fetchBusiness } from "../../actions/business_actions"; 4 | import PhotoIndex from "./photo_index"; 5 | import { withRouter } from "react-router-dom"; 6 | import { logout, clearPage, nextPage } from "../../actions/session_actions"; 7 | 8 | const mapStateToProps = (state, ownProps) => { 9 | return { 10 | photos: Object.values(state.photos), 11 | businessId: ownProps.match.params.businessId, 12 | currentUser: state.session.currentUser, 13 | business: state.business[ownProps.match.params.businessId] 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch, ownProps) => { 18 | return { 19 | fetchBizPhotos: businessId => dispatch(fetchBizPhotos(businessId)), 20 | fetchBusiness: businessId => dispatch(fetchBusiness(businessId)), 21 | clearPage: () => dispatch(clearPage()), 22 | nextPage: page => dispatch(nextPage(page)) 23 | }; 24 | }; 25 | 26 | export default withRouter( 27 | connect(mapStateToProps, mapDispatchToProps)(PhotoIndex) 28 | ); 29 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # Install JavaScript dependencies if using Yarn 22 | # system('bin/yarn') 23 | 24 | 25 | # puts "\n== Copying sample files ==" 26 | # unless File.exist?('config/database.yml') 27 | # cp 'config/database.yml.sample', 'config/database.yml' 28 | # end 29 | 30 | puts "\n== Preparing database ==" 31 | system! 'bin/rails db:setup' 32 | 33 | puts "\n== Removing old logs and tempfiles ==" 34 | system! 'bin/rails log:clear tmp:clear' 35 | 36 | puts "\n== Restarting application server ==" 37 | system! 'bin/rails restart' 38 | end 39 | -------------------------------------------------------------------------------- /frontend/components/photos/photo_upload_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { withRouter } from "react-router-dom"; 3 | import PhotoUpload from "./photo_upload"; 4 | import { createPhoto } from "../../actions/photo_actions"; 5 | import { 6 | clearErrors, 7 | clearPage, 8 | nextPage, 9 | logout 10 | } from "../../actions/session_actions"; 11 | import { fetchBusiness } from "../../actions/business_actions"; 12 | 13 | const mapStateToProps = (state, ownProps) => { 14 | return { 15 | currentUser: state.session.currentUser, 16 | intendedPage: state.intendedPage, 17 | business: state.business[ownProps.match.params.businessId] 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = (dispatch, ownProps) => { 22 | return { 23 | clearPage: () => dispatch(clearPage()), 24 | logout: () => dispatch(logout()), 25 | nextPage: page => dispatch(nextPage(page)), 26 | createPhoto: photo => dispatch(createPhoto(photo)), 27 | fetchBusiness: businessId => dispatch(fetchBusiness(businessId)) 28 | }; 29 | }; 30 | 31 | export default withRouter( 32 | connect(mapStateToProps, mapDispatchToProps)(PhotoUpload) 33 | ); 34 | -------------------------------------------------------------------------------- /frontend/components/business/search_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import Search from "./search"; 3 | import { fetchBusinesses } from "../../actions/business_actions"; 4 | import { fetchUser } from "../../actions/user_actions"; 5 | import { logout, clearPage, nextPage } from "../../actions/session_actions"; 6 | import { 7 | updateBounds, 8 | frontFilter, 9 | clearFilter 10 | } from "../../actions/filter_actions"; 11 | 12 | const mapStateToProps = (state, ownProps) => { 13 | return { 14 | business: Object.values(state.business), 15 | currentUser: state.session.currentUser, 16 | intendedPage: state.intendedPage, 17 | filters: state.filters 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = (dispatch, ownProps) => { 22 | return { 23 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)), 24 | logout: () => dispatch(logout()), 25 | clearPage: () => dispatch(clearPage()), 26 | nextPage: page => dispatch(nextPage(page)), 27 | updateBounds: bounds => dispatch(updateBounds(bounds)), 28 | clearFilter: () => dispatch(clearFilter()) 29 | }; 30 | }; 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(Search); 33 | -------------------------------------------------------------------------------- /frontend/components/business/business_show_container.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { 4 | fetchBusiness, 5 | fetchBusinesses, 6 | fetchReviewers 7 | } from "../../actions/business_actions"; 8 | import { logout, clearPage, nextPage } from "../../actions/session_actions"; 9 | import BusinessShow from "./business_show"; 10 | 11 | const mapStateToProps = (state, ownProps) => { 12 | let business = state.business[ownProps.match.params.businessId]; 13 | return { 14 | business, 15 | currentUser: state.session.currentUser, 16 | intendedPage: state.intendedPage, 17 | reviewers: state.reviewers 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = (dispatch, ownProps) => { 22 | return { 23 | fetchBusiness: businessId => dispatch(fetchBusiness(businessId)), 24 | logout: () => dispatch(logout()), 25 | clearPage: () => dispatch(clearPage()), 26 | nextPage: page => dispatch(nextPage(page)), 27 | fetchReviewers: businessId => dispatch(fetchReviewers(businessId)), 28 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)) 29 | }; 30 | }; 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(BusinessShow); 33 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Help 5 | <%= csrf_meta_tags %> 6 | <%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?key=AIzaSyBERKxjNFKIER1m_xsGh4AiuX-V92BguVo&libraries=places" %> 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | <%= javascript_include_tag 'application' %> 9 | 10 | <%= favicon_link_tag "favicon.ico" %> 11 | 21 | 22 | 23 | 24 | <%= yield %> 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/components/reviews/reviews_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import ReviewIndex from "./review_index"; 3 | import { fetchReview } from "../../actions/review_actions"; 4 | import { logout, clearPage, nextPage } from "../../actions/session_actions"; 5 | import { fetchUser } from "../../actions/user_actions"; 6 | import { fetchReviewers } from "../../actions/business_actions"; 7 | import { deleteReview } from "../../actions/review_actions"; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | reviews: Object.values(state.reviews), 12 | businessId: ownProps.businessId, 13 | currentUser: state.session.currentUser, 14 | intendedPage: state.intendedPage, 15 | reviewers: state.reviewers 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = (dispatch, ownProps) => { 20 | return { 21 | fetchReviewers: businessId => dispatch(fetchReviewers(businessId)), 22 | logout: () => dispatch(logout()), 23 | clearPage: () => dispatch(clearPage()), 24 | nextPage: page => dispatch(nextPage(page)), 25 | fetchUser: userId => dispatch(fetchUser(userId)), 26 | deleteReview: reviewId => dispatch(deleteReview(reviewId)) 27 | }; 28 | }; 29 | 30 | export default connect(mapStateToProps, mapDispatchToProps)(ReviewIndex); 31 | -------------------------------------------------------------------------------- /frontend/actions/business_actions.js: -------------------------------------------------------------------------------- 1 | import * as BusinessAPIUtil from "../util/business_api_util"; 2 | 3 | export const RECEIVE_BUSINESSES = "RECEIVE_BUSINESSES"; 4 | export const RECEIVE_BUSINESS = "RECEIVE_BUSINESS"; 5 | export const RECEIVE_REVIEWERS = "RECEIVE_USERS"; 6 | export const UPDATE_BOUNDS = "UPDATE_BOUNDS"; 7 | 8 | export const updateBounds = (bounds, value) => ({ 9 | type: UPDATE_BOUNDS, 10 | bounds, 11 | value 12 | }); 13 | 14 | export const receiveReviewers = reviewers => ({ 15 | type: RECEIVE_REVIEWERS, 16 | reviewers 17 | }); 18 | 19 | export const receiveBusinesses = businesses => ({ 20 | type: RECEIVE_BUSINESSES, 21 | businesses 22 | }); 23 | 24 | export const receiveBusiness = business => { 25 | return { 26 | type: RECEIVE_BUSINESS, 27 | business 28 | }; 29 | }; 30 | 31 | export const fetchBusinesses = filters => dispatch => 32 | BusinessAPIUtil.fetchBusinesses(filters).then(businesses => 33 | dispatch(receiveBusinesses(businesses)) 34 | ); 35 | 36 | export const fetchBusiness = businessId => dispatch => 37 | BusinessAPIUtil.fetchBusiness(businessId).then(business => 38 | dispatch(receiveBusiness(business)) 39 | ); 40 | 41 | export const fetchReviewers = businessId => dispatch => 42 | BusinessAPIUtil.fetchReviewers(businessId).then(reviewers => 43 | dispatch(receiveReviewers(reviewers)) 44 | ); 45 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as SessionApiUtil 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 | export const NEXT_PAGE = "NEXT_PAGE"; 7 | export const CLEAR_PAGE = "ClEAR_PAGE"; 8 | 9 | export const nextPage = page => ({ 10 | type: NEXT_PAGE, 11 | page 12 | }); 13 | 14 | export const clearPage = () => ({ 15 | type: CLEAR_PAGE 16 | }); 17 | 18 | export const receiveCurrentUser = currentUser => ({ 19 | type: RECEIVE_CURRENT_USER, 20 | currentUser 21 | }); 22 | 23 | export const receiveErrors = errors => ({ 24 | type: RECEIVE_ERRORS, 25 | errors 26 | }); 27 | 28 | export const clearErrors = () => ({ 29 | type: CLEAR_ERRORS 30 | }); 31 | 32 | export const login = user => dispatch => 33 | SessionApiUtil.login(user).then( 34 | usr => dispatch(receiveCurrentUser(usr)), 35 | errors => dispatch(receiveErrors(errors.responseJSON)) 36 | ); 37 | 38 | export const logout = () => dispatch => 39 | SessionApiUtil.logout().then(user => dispatch(receiveCurrentUser(null))); 40 | 41 | export const signup = user => dispatch => 42 | SessionApiUtil.signup(user).then( 43 | usr => dispatch(receiveCurrentUser(usr)), 44 | errors => dispatch(receiveErrors(errors.responseJSON)) 45 | ); 46 | -------------------------------------------------------------------------------- /frontend/components/business/search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BusinessIndex from "./business_index"; 3 | import BusinessMap from "./business_map"; 4 | import NavBar from "../NavBar/NavBar"; 5 | 6 | class Search extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | window.scrollTo(0, 0); 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 | 23 | 24 |
      25 |
    • 26 | 34 |
    • 35 | 36 |
    • 37 | 41 |
    • 42 |
    43 |
    44 | ); 45 | } 46 | } 47 | 48 | export default Search; 49 | -------------------------------------------------------------------------------- /app/assets/stylesheets/filter_search.scss: -------------------------------------------------------------------------------- 1 | 2 | .filter-box { 3 | height: 70px; 4 | background-color: #F5F5F5; 5 | width: 100%; 6 | position: absolute; 7 | left: 0px; 8 | top: 92px; 9 | 10 | } 11 | 12 | .smaller-filter-box { 13 | width: 960px; 14 | display: flex; 15 | justify-content: space-between; 16 | margin: auto; 17 | padding-left: 20px; 18 | } 19 | 20 | .filter-box ul { 21 | margin-top: 25px; 22 | } 23 | 24 | 25 | .price-filter { 26 | display: flex; 27 | } 28 | 29 | .price-label { 30 | cursor: pointer; 31 | border: 1px solid #CCCCCC; 32 | padding: 10px; 33 | font-family: sans-serif; 34 | font-size: 14px; 35 | padding-left: 12px; 36 | padding-right: 12px; 37 | } 38 | 39 | .first-price { 40 | border-radius: 4px 0px 0px 4px; 41 | 42 | } 43 | 44 | .last-price { 45 | border-radius: 0px 4px 4px 0px; 46 | } 47 | 48 | .price { 49 | -webkit-appearance: none; 50 | } 51 | 52 | .green { 53 | background-color: #C6F6A4; 54 | border: 1px solid #419446; 55 | color: #419446; 56 | } 57 | 58 | .noise-filter { 59 | display: flex; 60 | font-family: sans-serif; 61 | font-size: 14px; 62 | } 63 | 64 | .noise-level { 65 | font-weight: bold; 66 | margin-right: 10px; 67 | } 68 | .delivery-filter { 69 | display: flex; 70 | } 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | // 92 | -------------------------------------------------------------------------------- /frontend/components/photos/photo_show_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { fetchPhoto, fetchBizPhotos } from "../../actions/photo_actions"; 3 | import { fetchBusiness } from "../../actions/business_actions"; 4 | import PhotoShow from "./photo_show"; 5 | import { withRouter } from "react-router-dom"; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | let photoNumber; 9 | let photo; 10 | let photoId = parseInt(ownProps.match.params.photoId); 11 | let photos = Object.values(state.photos); 12 | for (let i = 0; i < photos.length; i++) { 13 | if (photos[i].id === photoId) { 14 | photo = photos[i]; 15 | photoNumber = i; 16 | break; 17 | } 18 | } 19 | 20 | return { 21 | photos, 22 | businessId: ownProps.match.params.businessId, 23 | currentUser: state.session.currentUser, 24 | business: state.business[ownProps.match.params.businessId], 25 | photo, 26 | photoNumber, 27 | photoId 28 | }; 29 | }; 30 | 31 | const mapDispatchToProps = (dispatch, ownProps) => { 32 | return { 33 | fetchBizPhotos: businessId => dispatch(fetchBizPhotos(businessId)), 34 | fetchBusiness: businessId => dispatch(fetchBusiness(businessId)), 35 | fetchPhoto: photoId => dispatch(fetchPhoto(photoId)) 36 | }; 37 | }; 38 | 39 | export default withRouter( 40 | connect(mapStateToProps, mapDispatchToProps)(PhotoShow) 41 | ); 42 | -------------------------------------------------------------------------------- /app/controllers/api/reviews_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ReviewsController < ApplicationController 2 | 3 | 4 | def index 5 | @reviews = Review.all 6 | end 7 | 8 | def new 9 | end 10 | 11 | def create 12 | @review = Review.new(review_params) 13 | @review.business_id = params[:business_id] 14 | @review.user_id = current_user.id 15 | if @review.save 16 | business = @review.business 17 | render json: business, include: [:reviews] 18 | else 19 | render json: @review.errors.full_messages, status: 422 20 | end 21 | end 22 | 23 | def edit 24 | @review = current_user.reviews.find(params[:id]) 25 | end 26 | 27 | def update 28 | @review = current_user.reviews.find(params[:id]) 29 | if @review.update(review_params) 30 | business = @review.business_id 31 | render json: business, include: [:reviews] 32 | else 33 | render json: @review.errors.full_messages, status: 422 34 | end 35 | end 36 | 37 | def show 38 | @review = Review.find(params[:id]) 39 | end 40 | 41 | def destroy 42 | @review = current_user.reviews.find(params[:id]) 43 | business = @review.business_id 44 | @review.destroy! 45 | render json: @review 46 | end 47 | 48 | private 49 | def review_params 50 | params.require(:review).permit(:rating, :price_range, 51 | :body, :delivery, :noise_level, :created_at) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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 `rails 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 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: 4daa1eb30d1dd47759e273940e822323c25368d00b4b53ad4ea9e5096f4eea4aa9a9a25df5dfd418e4bda4c20f847a7af3f6903da9f0595d5bd3511f94b26318 22 | 23 | test: 24 | secret_key_base: ff41456659a53bede2171f6353349a1877c6095155562afe58c28b5f9499ac938dbf92f776fb8c8a6c1c91f23f8911532e9fd2dab8403123010053b0ec2b4b1b 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /frontend/actions/photo_actions.js: -------------------------------------------------------------------------------- 1 | import * as PhotoAPIUtil from '../util/photo_api_util'; 2 | 3 | export const RECEIVE_PHOTOS = "RECEIVE_PHOTOS"; 4 | export const RECEIVE_PHOTO = "RECEIVE_PHOTO"; 5 | export const REMOVE_PHOTO = "REMOVE_PHOTO"; 6 | 7 | export const receivePhotos = photos => ({ 8 | type: RECEIVE_PHOTOS, 9 | photos 10 | }); 11 | 12 | export const receivePhoto = photo => ({ 13 | type: RECEIVE_PHOTO, 14 | photo 15 | }); 16 | 17 | export const removePhoto = photoId => ({ 18 | type: REMOVE_PHOTO, 19 | photoId 20 | }); 21 | 22 | export const fetchBizPhotos = (businessId) => dispatch => ( 23 | PhotoAPIUtil.fetchBizPhotos(businessId).then( 24 | photos => dispatch(receivePhotos(photos)) 25 | ) 26 | ); 27 | 28 | export const fetchPhoto = (photoId) => dispatch => ( 29 | PhotoAPIUtil.fetchPhoto(photoId).then( 30 | photo => dispatch(receivePhoto(photo)) 31 | ) 32 | ); 33 | 34 | export const createPhoto = data => dispatch => ( 35 | PhotoAPIUtil.createPhoto(data).then( 36 | photo => dispatch(receivePhoto(photo)) 37 | ) 38 | ); 39 | 40 | export const deletePhoto = photoId => dispatch => ( 41 | PhotoAPIUtil.deletePhoto(photoId).then( 42 | photo => dispatch(removePhoto(photo.id)) 43 | ) 44 | ); 45 | 46 | export const updatePhoto = updatedPhoto => dispatch => ( 47 | PhotoAPIUtil.updatePhoto(updatedPhoto).then( 48 | photo => dispatch(receivePhoto(photo)) 49 | ) 50 | ); 51 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | validates :email, :password_digest, :session_token, presence: true 3 | validates :first_name, :last_name, :zip_code, presence: true 4 | validates :password, length: { minimum: 6, allow_nil: true } 5 | validates :email, uniqueness: true 6 | after_initialize :ensure_session_token 7 | attr_reader :password 8 | 9 | has_many :reviews 10 | has_many :photos 11 | 12 | def self.find_by_credentials(email, password) 13 | user = User.find_by(email: email) 14 | user && user.is_password?(password) ? user : nil 15 | end 16 | 17 | def is_password?(password) 18 | BCrypt::Password.new(self.password_digest).is_password?(password) 19 | end 20 | 21 | def password=(password) 22 | @password = password 23 | self.password_digest = BCrypt::Password.create(password) 24 | end 25 | 26 | def reset_session_token! 27 | self.session_token = SecureRandom.urlsafe_base64(16) 28 | self.save! 29 | self.session_token 30 | end 31 | 32 | def ensure_session_token 33 | self.session_token ||= SecureRandom.urlsafe_base64(16) 34 | end 35 | 36 | has_attached_file :image, default_url: "https://s3.amazonaws.com/helpcoreyladovskyprojectdev/users/images/000/000/005/original/default.jpg", styles: { thumb: '100x100', croppable: '600x600>', big: '1000x1000>' } 37 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 38 | 39 | end 40 | -------------------------------------------------------------------------------- /frontend/components/session/session_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { 3 | login, 4 | signup, 5 | clearErrors, 6 | clearPage 7 | } from "../../actions/session_actions"; 8 | import SessionForm from "./session_form"; 9 | import { withRouter } from "react-router-dom"; 10 | 11 | const mapStateToProps = (state, ownProps) => { 12 | let formType; 13 | if (ownProps.location.pathname === "/login") { 14 | formType = "/login"; 15 | } else { 16 | formType = "/signup"; 17 | } 18 | 19 | return { 20 | loggedIn: Boolean(state.session.email), 21 | errors: state.errors.session, 22 | intendedPage: state.intendedPage, 23 | formType 24 | }; 25 | }; 26 | 27 | const mapDispatchToProps = (dispatch, ownProps) => { 28 | if (ownProps.location.pathname === "/login") { 29 | return { 30 | processForm: user => dispatch(login(user)), 31 | login: user => dispatch(login(user)), 32 | clearErrors: () => dispatch(clearErrors()), 33 | clearPage: () => dispatch(clearPage()) 34 | }; 35 | } else { 36 | return { 37 | processForm: user => dispatch(signup(user)), 38 | login: user => dispatch(login(user)), 39 | clearErrors: () => dispatch(clearErrors()), 40 | clearPage: () => dispatch(clearPage()) 41 | }; 42 | } 43 | }; 44 | 45 | export default withRouter( 46 | connect(mapStateToProps, mapDispatchToProps)(SessionForm) 47 | ); 48 | -------------------------------------------------------------------------------- /app/models/business.rb: -------------------------------------------------------------------------------- 1 | class Business < ApplicationRecord 2 | validates :name, :address, :phone_number, :cuisine, presence: true 3 | 4 | has_many :reviews 5 | 6 | has_many :photos 7 | 8 | has_many :reviewers, through: :reviews, source: :user 9 | 10 | has_attached_file :image, styles: { thumb: '100x100', croppable: '600x600>', big: '1000x1000>' } 11 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 12 | 13 | def in_bounds(bounds) 14 | 15 | return true if bounds == "" || bounds.nil? 16 | ky = 4000 / 360 17 | kx = Math.cos(Math::PI * bounds[0].to_f / 180.0) * ky 18 | dx = (bounds[1].to_f - self.lng).abs * kx 19 | dy = (bounds[0].to_f - self.lat).abs * ky 20 | Math.sqrt(dx * dx + dy * dy ) <= 0.8 21 | end 22 | 23 | def average_rating 24 | return 0 unless !!self.reviews 25 | (self.reviews.average(:rating).to_f * 2.to_f).round / 2.0 26 | end 27 | 28 | def review_count 29 | return 0 unless self.reviews 30 | self.reviews.count 31 | end 32 | 33 | def price_range 34 | return 0 unless self.reviews 35 | self.reviews.average(:price_range).to_i 36 | end 37 | 38 | def delivery 39 | return false unless self.reviews 40 | self.reviews.where(delivery: [true]).count > self.reviews.where(delivery: [false]).count 41 | end 42 | 43 | def noise_level 44 | return 0 unless self.reviews 45 | self.reviews.average(:noise_level).to_i 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /frontend/util/marker_manager.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | export default class MarkerManager { 4 | constructor(map) { 5 | this.map = map; 6 | this.markers = {}; 7 | } 8 | 9 | updateMarkers(businesses) { 10 | if (businesses === "clear") { 11 | for (let key in this.markers) { 12 | this.markers[key].setMap(null); 13 | } 14 | } else { 15 | const businessesObj = {}; 16 | for (let key in this.markers) { 17 | this.markers[key].setMap(null); 18 | } 19 | businesses.forEach(business => { 20 | this.createMarkerFromBusiness(business); 21 | }); 22 | } 23 | } 24 | 25 | createMarkerFromBusiness(business) { 26 | const position = new google.maps.LatLng(business.lat, business.lng); 27 | const marker = new google.maps.Marker({ 28 | content: `${business.image}, ${business.name}, ${business.address}`, 29 | position, 30 | map: this.map, 31 | businessId: business.id, 32 | animation: google.maps.Animation.DROP 33 | }); 34 | let img = business.image; 35 | let infowindow = new google.maps.InfoWindow({ 36 | content: 37 | ` ` + 38 | business.name + 39 | " " + 40 | business.address 41 | }); 42 | 43 | marker.addListener("click", function() { 44 | infowindow.open(marker.get("map"), marker); 45 | }); 46 | 47 | this.markers[marker.businessId] = marker; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/actions/review_actions.js: -------------------------------------------------------------------------------- 1 | import * as ReviewAPIUtil from "../util/review_api_util"; 2 | import { receiveErrors, RECEIVE_ERRORS } from "./session_actions"; 3 | 4 | export const RECEIVE_REVIEWS = "RECEIVE_REVIEWS"; 5 | export const RECEIVE_REVIEW = "RECEIVE_REVIEW"; 6 | export const REMOVE_REVIEW = "REMOVE_REVIEW"; 7 | 8 | export const receiveReviews = reviews => ({ 9 | type: RECEIVE_REVIEWS, 10 | reviews 11 | }); 12 | 13 | export const receiveReview = review => ({ 14 | type: RECEIVE_REVIEW, 15 | review 16 | }); 17 | 18 | export const removeReview = reviewId => ({ 19 | type: REMOVE_REVIEW, 20 | reviewId 21 | }); 22 | 23 | export const fetchReviews = businessId => dispatch => 24 | ReviewAPIUtil.fetchReviews(businessId).then(reviews => 25 | dispatch(receiveReviews(reviews)) 26 | ); 27 | 28 | export const fetchReview = reviewId => dispatch => 29 | ReviewAPIUtil.fetchReview(reviewId).then(review => 30 | dispatch(receiveReview(review)) 31 | ); 32 | 33 | export const createReview = review => dispatch => 34 | ReviewAPIUtil.createReview(review).then( 35 | rev => dispatch(receiveReview(rev)), 36 | errors => dispatch(receiveErrors(errors.responseJSON)) 37 | ); 38 | 39 | export const updateReview = reviewId => dispatch => 40 | ReviewAPIUtil.updateReview(reviewId).then( 41 | review => dispatch(receiveReview(review)), 42 | errors => dispatch(receiveErrors(errors.responseJSON)) 43 | ); 44 | 45 | export const deleteReview = reviewId => dispatch => 46 | ReviewAPIUtil.deleteReview(reviewId).then(review => 47 | dispatch(removeReview(review.id)) 48 | ); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Help", 3 | "private": true, 4 | "dependencies": { 5 | "ajv": "^6.9.1", 6 | "babel-core": "^6.26.0", 7 | "babel-loader": "^7.1.2", 8 | "babel-preset-es2015": "^6.24.1", 9 | "babel-preset-react": "^6.24.1", 10 | "cities": "^1.1.2", 11 | "dateformat": "^3.0.2", 12 | "geocoder": "^0.2.3", 13 | "lodash": "^4.17.4", 14 | "node-geocoder": "^3.21.1", 15 | "parse-address": "0.0.8", 16 | "react": "^16.0.0", 17 | "react-dom": "^16.0.0", 18 | "react-native-geocoder": "^0.5.0", 19 | "react-places-autocomplete": "^5.4.3", 20 | "react-redux": "^5.1.2", 21 | "react-router-dom": "^4.2.2", 22 | "redux": "^4.1.2", 23 | "redux-logger": "^3.0.6", 24 | "redux-thunk": "^2.4.1", 25 | "underscore": "^1.8.3", 26 | "url-loader": "^0.6.2", 27 | "webpack": "^3.8.1", 28 | "zipcodes": "^6.0.0" 29 | }, 30 | "engines": { 31 | "node": "4.1.1", 32 | "npm": "2.1.x" 33 | }, 34 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 35 | "version": "1.0.0", 36 | "main": "index.js", 37 | "directories": { 38 | "lib": "lib", 39 | "test": "test" 40 | }, 41 | "scripts": { 42 | "test": "echo \"Error: no test specified\" && exit 1", 43 | "start": "webpack --watch", 44 | "postinstall": "webpack" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/coreyladovsky/help.git" 49 | }, 50 | "keywords": [], 51 | "author": "", 52 | "license": "ISC", 53 | "bugs": { 54 | "url": "https://github.com/coreyladovsky/help/issues" 55 | }, 56 | "homepage": "https://github.com/coreyladovsky/help#readme", 57 | "devDependencies": { 58 | "file-loader": "^1.1.5", 59 | "inline-environment-variables-webpack-plugin": "^1.2.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /frontend/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LandingContainer from './landing/landing_container'; 3 | import SessionFormContainer from './session/session_form_container'; 4 | import SearchContainer from './business/search_container'; 5 | import { Route, Switch } from 'react-router-dom'; 6 | import { AuthRoute, ProtectedRoute } from '../util/route_util'; 7 | import ReviewsContainer from './reviews/reviews_container'; 8 | import ReviewFormContainer from './reviews/review_form_container'; 9 | import BusinessShowContainer from './business/business_show_container'; 10 | import PhotoUploadContainer from './photos/photo_upload_container'; 11 | import PhotosContainer from './photos/photos_container'; 12 | import { Footer } from './footer.jsx'; 13 | 14 | 15 | const App = () => 16 | { 17 | 18 | return( 19 |
    20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
    38 | ); 39 | }; 40 | export default App; 41 | -------------------------------------------------------------------------------- /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 public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 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 | -------------------------------------------------------------------------------- /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. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | 49 | 50 | # Raises error for missing translations 51 | # config.action_view.raise_on_missing_translations = true 52 | 53 | # Use an evented file watcher to asynchronously detect changes in source code, 54 | # routes, locales, etc. This feature depends on the listen gem. 55 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 56 | end 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/commments.scss: -------------------------------------------------------------------------------- 1 | .user-info-comment { 2 | display: flex; 3 | margin-top: 10px; 4 | 5 | } 6 | 7 | .user-profile-pic-comment { 8 | height: 62px; 9 | width: 62px; 10 | border-radius: 5px; 11 | margin-top: 14px; 12 | object-fit: cover; 13 | } 14 | 15 | .user-profile-pic-comment-li { 16 | height: 62px; 17 | width: 62px; 18 | border-radius: 5px; 19 | 20 | } 21 | 22 | .user-profile-name-comment { 23 | margin-left: 8px; 24 | font-size: 14px; 25 | color: #0D7ABE; 26 | font-family: sans-serif; 27 | font-weight: bold; 28 | margin-top: 14px; 29 | 30 | 31 | } 32 | 33 | .zip-code-comment { 34 | font-size: 12px; 35 | font-weight: bold; 36 | font-family: sans-serif; 37 | margin-left: 70px; 38 | 39 | } 40 | 41 | .user-review-rating { 42 | width: 110px; 43 | height: 27px; 44 | } 45 | 46 | .review-stars-and-date { 47 | display: flex; 48 | margin-top: 16px; 49 | } 50 | 51 | .review-date-comments { 52 | color: #898989; 53 | font-size: 14px; 54 | font-family: sans-serif; 55 | margin-top: 4px; 56 | } 57 | 58 | .review-body-comment { 59 | margin-top: 4px; 60 | font-family: sans-serif; 61 | } 62 | 63 | .review-info-comment { 64 | width: 400px; 65 | padding-left: 25px; 66 | 67 | 68 | } 69 | 70 | .review-container-comment{ 71 | display: flex; 72 | justify-content: space-between; 73 | border-top: 1px solid #E6E6E6; 74 | margin-bottom: 20px; 75 | width: 670px; 76 | 77 | } 78 | 79 | .editors { 80 | display: flex; 81 | margin-top: 15px; 82 | margin-left: 125px; 83 | } 84 | 85 | .edit-review-link { 86 | color: #0073BB; 87 | font-size: 13px; 88 | font-family: sans-serif; 89 | 90 | padding-top: 10px 91 | } 92 | 93 | .edit-review-link:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | 98 | .delete-link-button { 99 | color: #0073BB; 100 | font-size: 13px; 101 | font-family: sans-serif; 102 | 103 | margin-left: 30px; 104 | } 105 | 106 | .delete-link-button:hover { 107 | text-decoration: underline; 108 | } 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | // 123 | -------------------------------------------------------------------------------- /frontend/components/business/business_index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BusinessIndexItem from "./business_index_item"; 3 | import { shuffle } from "underscore"; 4 | 5 | class BusinessIndex extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.compareValues = this.compareValues.bind(this); 9 | } 10 | 11 | compareValues(arg1, arg2) { 12 | if (arg1 === "false") return true; 13 | if (arg1 === "true") { 14 | return true === arg2; 15 | } 16 | return parseInt(arg1) >= arg2; 17 | } 18 | 19 | render() { 20 | let none = ( 21 |

    22 | Sorry No Businesses Found,
    23 | Please Try an Alternative Search
    {" "} 24 |

    25 | ); 26 | 27 | let count = 0; 28 | const businesses = this.props.business.map((business, idx) => { 29 | if (!this.props.filters) { 30 | count++; 31 | if (count <= 20) { 32 | return ( 33 | 41 | ); 42 | } 43 | } else if ( 44 | Object.keys(this.props.filters).every(filter => 45 | this.compareValues(this.props.filters[filter], business[filter]) 46 | ) 47 | ) { 48 | count++; 49 | if (count <= 20) { 50 | return ( 51 | 59 | ); 60 | } 61 | } 62 | }); 63 | 64 | return ( 65 |
    66 |
      67 | {businesses.length > 0 ? businesses : none} 68 |
    69 |
    70 | ); 71 | } 72 | } 73 | 74 | export default BusinessIndex; 75 | -------------------------------------------------------------------------------- /frontend/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const Footer = props => { 5 | return ( 6 |
    7 | 63 |
    64 |
    65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /frontend/components/NavBar/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import SearchFormContainer from "../search_form_container"; 4 | 5 | class NavBar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.logButton = this.logButton.bind(this); 10 | this.loginLink = this.loginLink.bind(this); 11 | this.clickHandler = this.clickHandler.bind(this); 12 | } 13 | 14 | clickHandler(event) { 15 | this.props.nextPage("/reviews"); 16 | } 17 | 18 | logButton() { 19 | if (this.props.currentUser) { 20 | return ( 21 | 24 | ); 25 | } else { 26 | return ( 27 | 28 | Sign Up 29 | 30 | ); 31 | } 32 | } 33 | 34 | loginLink() { 35 | if (this.props.currentUser) { 36 | return ( 37 | 38 | Home 39 | 40 | ); 41 | } else { 42 | return ( 43 | 44 | Log In 45 | 46 | ); 47 | } 48 | } 49 | 50 | render() { 51 | return ( 52 |
    53 |
    54 |
    55 |
      56 |
    • 57 |
      58 | 59 | help 60 | 61 |
      62 |
    • 63 | 64 |
    • {this.logButton()}
    • 65 |
    66 |
    67 |
    68 |
    69 |
      70 |
    • 71 |
    • {this.loginLink()}
    • 72 |
    73 |
    74 |
    75 |
    76 |
    77 | ); 78 | } 79 | } 80 | 81 | export default NavBar; 82 | -------------------------------------------------------------------------------- /frontend/components/business/business_map.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import ReactDom from "react-dom"; 4 | import MarkerManager from "../../util/marker_manager"; 5 | 6 | class BusinessMap extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.compareValues = this.compareValues.bind(this); 10 | } 11 | 12 | componentDidMount() { 13 | let mapOptions = { 14 | center: { lat: 40.7513, lng: -73.983665 }, 15 | zoom: 13 16 | }; 17 | this.mapNode = document.getElementById("map-container"); 18 | this.map = new google.maps.Map(this.mapNode, mapOptions); 19 | this.MarkerManager = new MarkerManager(this.map); 20 | } 21 | 22 | compareValues(arg1, arg2) { 23 | if (arg1 === "false") return true; 24 | if (arg1 === "true") { 25 | return true === arg2; 26 | } 27 | return parseInt(arg1) >= arg2; 28 | } 29 | 30 | componentDidUpdate() { 31 | if (this.props.business.length !== 0) { 32 | let mapOptions = { 33 | center: { 34 | lat: this.props.business[0].lat, 35 | lng: this.props.business[0].lng 36 | }, 37 | zoom: 11 38 | }; 39 | 40 | this.mapNode = document.getElementById("map-container"); 41 | this.map = new google.maps.Map(this.mapNode, mapOptions); 42 | this.MarkerManager = new MarkerManager(this.map); 43 | 44 | let bizzys = []; 45 | let count = 0; 46 | this.props.business.forEach((bus, i) => { 47 | if (!this.props.filters) { 48 | bizzys.push(bus); 49 | } else if ( 50 | Object.keys(this.props.filters).every(filter => 51 | this.compareValues( 52 | this.props.filters[filter], 53 | this.props.business[i][filter] 54 | ) 55 | ) 56 | ) { 57 | bizzys.push(this.props.business[i]); 58 | } 59 | }); 60 | 61 | this.MarkerManager.updateMarkers(bizzys.slice(0, 20)); 62 | } else { 63 | this.MarkerManager.updateMarkers("clear"); 64 | } 65 | } 66 | 67 | render() { 68 | return ( 69 |
    (this.mapNode = map)} 73 | /> 74 | ); 75 | } 76 | } 77 | 78 | export default withRouter(BusinessMap); 79 | -------------------------------------------------------------------------------- /app/assets/stylesheets/photo_show.scss: -------------------------------------------------------------------------------- 1 | .photo-modal { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | display: flex; 8 | justify-content: center; 9 | flex-direction: column; 10 | z-index: 1; 11 | background-image: image_url("transpBlack75.png"); 12 | } 13 | 14 | .photo-content { 15 | background-image: image_url("transpBlack90.png"); 16 | display: flex; 17 | // width: 1100px; 18 | width: 900px; 19 | max-height: 905px; 20 | 21 | z-index: 2; 22 | // min-width: 962px; 23 | min-height: 615px; 24 | margin-top: 50px; 25 | margin-bottom: 50px; 26 | margin: 0 auto; 27 | border-radius: 5px; 28 | } 29 | 30 | .photo-of-photo-show { 31 | position: relative; 32 | // width: 550px; 33 | width: 1000px; 34 | height: 615px; 35 | } 36 | 37 | .photo-image-show { 38 | z-index: 2; 39 | height: 100%; 40 | object-fit: cover; 41 | } 42 | 43 | .photo-image-show:hover { 44 | cursor: pointer; 45 | } 46 | 47 | .left-scroll { 48 | z-index: 3; 49 | color: white; 50 | font-size: 60px; 51 | // width: 10%; 52 | width: 20%; 53 | text-align: center; 54 | } 55 | 56 | .fa-angle-right { 57 | padding-top: 250%; 58 | } 59 | .fa-angle-left { 60 | padding-top: 250%; 61 | } 62 | 63 | 64 | .left-scroll:hover { 65 | cursor: pointer; 66 | } 67 | .right-scroll:hover { 68 | cursor: pointer; 69 | } 70 | 71 | .right-scroll { 72 | z-index: 3; 73 | color: white; 74 | font-size: 60px; 75 | // width: 10%; 76 | width: 20%; 77 | text-align: center; 78 | } 79 | 80 | .close-modal { 81 | float: right; 82 | color: #666666; 83 | font-family: sans-serif; 84 | font-size: 16px; 85 | display: inline-block; 86 | margin-bottom: 8px; 87 | } 88 | 89 | .close-container { 90 | // width: 1100px; 91 | width: 900px; 92 | margin: 0 auto; 93 | } 94 | 95 | .close-modal i { 96 | margin-left: 5px; 97 | } 98 | .close-modal:hover { 99 | cursor: pointer; 100 | color: white; 101 | } 102 | 103 | .trans-photo-bar { 104 | position: absolute; 105 | bottom: 0; 106 | min-width: 100%; 107 | min-height: 20px; 108 | z-index: 4; 109 | background-image: image_url("transpBlack75.png"); 110 | color: #666666; 111 | font-family: sans-serif; 112 | text-align: center; 113 | font-size: 12px; 114 | padding-top: 10px; 115 | } 116 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby "2.6.5" 3 | 4 | git_source(:github) do |repo_name| 5 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 6 | "https://github.com/#{repo_name}.git" 7 | end 8 | 9 | gem 'mimemagic', github: 'mimemagicrb/mimemagic', ref: '01f92d86d15d85cfd0f20dabd025dcbd36a8a60f' 10 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 11 | gem 'rails', '~> 5.1.4' 12 | gem 'font-awesome-rails' 13 | # Use postgresql as the database for Active Record 14 | gem 'pg', '~> 0.18' 15 | # Use Puma as the app server 16 | gem 'puma', '~> 3.7' 17 | # Use SCSS for stylesheets 18 | gem 'sass-rails', '~> 5.0' 19 | # Use Uglifier as compressor for JavaScript assets 20 | gem 'uglifier', '>= 1.3.0' 21 | # See https://github.com/rails/execjs#readme for more supported runtimes 22 | # gem 'therubyracer', platforms: :ruby 23 | 24 | # Use CoffeeScript for .coffee assets and views 25 | gem 'coffee-rails', '~> 4.2' 26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 27 | gem 'jbuilder', '~> 2.5' 28 | # Use Redis adapter to run Action Cable in production 29 | # gem 'redis', '~> 3.0' 30 | # Use ActiveModel has_secure_password 31 | gem 'bcrypt', '~> 3.1.7' 32 | gem 'Indirizzo', require: "indirizzo" 33 | # Use Capistrano for deployment 34 | # gem 'capistrano-rails', group: :development 35 | gem "paperclip", '~> 5.0.0' 36 | gem 'figaro' 37 | gem 'aws-sdk', '< 3.0' 38 | gem 'jquery-rails' 39 | group :development, :test do 40 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 41 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 42 | # Adds support for Capybara system testing and selenium driver 43 | gem 'capybara', '~> 2.13' 44 | gem 'selenium-webdriver' 45 | gem 'better_errors' 46 | gem 'binding_of_caller' 47 | gem 'pry-rails' 48 | gem 'annotate' 49 | end 50 | 51 | group :development do 52 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 53 | gem 'web-console', '>= 3.3.0' 54 | gem 'listen', '>= 3.0.5', '< 3.2' 55 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 56 | gem 'spring' 57 | gem 'spring-watcher-listen', '~> 2.0.0' 58 | end 59 | 60 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 61 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 62 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/photo-index.scss: -------------------------------------------------------------------------------- 1 | .container-photo-index { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .main-photo-index { 7 | margin-left: 27px; 8 | align-self: center; 9 | } 10 | 11 | .title-biz-index { 12 | font-size: 27px; 13 | font-family: sans-serif; 14 | font-weight: bold; 15 | display: flex; 16 | margin-top: 45px; 17 | } 18 | .title-biz-index :last-child { 19 | margin-left: 8px; 20 | } 21 | 22 | .photo-index-ul { 23 | list-style-type: none; 24 | max-width: 975px; 25 | margin-bottom: 40px; 26 | } 27 | 28 | .photo-index-header-container { 29 | max-width: 965px; 30 | display: flex; 31 | justify-content: space-between; 32 | margin-top: 10px; 33 | padding-bottom: 45px; 34 | border-bottom: 1px solid #E6E6E6; 35 | margin-bottom: 13px; 36 | } 37 | 38 | .index-image { 39 | height: 150px; 40 | width: 150px; 41 | border-radius: 5px; 42 | object-fit: cover; 43 | } 44 | 45 | .biz-info-photo-index { 46 | display: flex; 47 | } 48 | 49 | .rating-and-reviews-photo-index { 50 | display: flex; 51 | } 52 | 53 | .index-li { 54 | margin: 6px; 55 | height: 150px; 56 | width: 150px; 57 | position: relative; 58 | box-sizing: border-box; 59 | display: inline-block; 60 | } 61 | 62 | .index-li:hover { 63 | cursor: pointer; 64 | } 65 | 66 | .biz-image-photo-index { 67 | height: 30px; 68 | width: 30px; 69 | margin-top: 5px; 70 | margin-right: 5px; 71 | } 72 | 73 | .biz-name-link-photos { 74 | color: #0073BB; 75 | font-size: 14px; 76 | font-family: sans-serif; 77 | font-weight: bold; 78 | margin-top: 4px; 79 | } 80 | 81 | .biz-name-link-photos:hover { 82 | text-decoration: underline; 83 | } 84 | 85 | .biz-review-text-photos { 86 | color: #666666; 87 | font-size: 12px; 88 | font-family: sans-serif; 89 | margin-left: 4px; 90 | padding-top: 4px; 91 | } 92 | 93 | .biz-review-count-photos { 94 | color: #666666; 95 | font-size: 12px; 96 | font-family: sans-serif; 97 | padding-top: 4px; 98 | margin-left: 4px; 99 | } 100 | 101 | .add-photo-photos { 102 | width: 148px; 103 | height: 35px; 104 | border: 1px solid #8D0005; 105 | background-color: #D70306; 106 | border-radius: 4px; 107 | color: white; 108 | font-family: sans-serif; 109 | font-weight: bold; 110 | text-align: center; 111 | font-size: 15px; 112 | display: table; 113 | 114 | } 115 | .add-photo-photos:hover { 116 | background-color: #E02E33; 117 | } 118 | .photo-button-photos { 119 | display: table-cell; 120 | vertical-align: middle; 121 | 122 | } 123 | 124 | 125 | 126 | .photo-index-rating { 127 | height: 20px; 128 | width: 80px; 129 | padding-bottom: 5px; 130 | } 131 | -------------------------------------------------------------------------------- /app/controllers/api/businesses_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BusinessesController < ApplicationController 2 | def index 3 | # @businesses = Business.in_bounds(params[:bounds]).includes(:reviews) # running a class method to get average rating for all of these businesses 4 | # businesses = params[:bounds] ? Business.in_bounds(params[:bounds]) : Business.all 5 | # @businesses = businesses.includes(:reviews) || {} 6 | 7 | @businesses = Business.all.includes(:reviews).includes(:photos) 8 | @businesses = @businesses.where(["lower(name) LIKE ? OR lower(cuisine) LIKE ?", "%#{business_params[:name]}%", "%#{business_params[:cuisine]}%" ]) if business_params[:cuisine] !="" 9 | quality_bizs = [] 10 | delivers = [] 11 | @businesses.each do |business| 12 | if business_params[:delivery] == "true" 13 | next if business.delivery == false 14 | end 15 | 16 | 17 | if (business.price_range <= business_params[:price_range].to_i) && 18 | (business.noise_level <= business_params[:noise_level].to_i) && 19 | 20 | if business.in_bounds(params[:business][:bounds]) 21 | quality_bizs << business 22 | end 23 | end 24 | end 25 | @businesses = quality_bizs.concat(delivers) 26 | end 27 | 28 | def show 29 | @business = Business.find(params[:id]) 30 | end 31 | 32 | def create 33 | @business = Business.new(business_params) 34 | 35 | if @businesses.save 36 | render "api/business/show" 37 | else 38 | render json: @business.errors.full_messages, status: 422 39 | end 40 | end 41 | 42 | def update 43 | @business = Business.find(params[:id]) 44 | 45 | if @business.update(business_params) 46 | render "api/business/show" 47 | else 48 | render json: @business.errors.full_messages, status: 422 49 | end 50 | end 51 | 52 | def destroy 53 | @business = Business.find(params[:id]) 54 | 55 | if @post.destroy 56 | render "api/business/index" 57 | else 58 | render json: @post.errors.full_messages, status: 422 59 | end 60 | end 61 | 62 | def reviewers 63 | @business = Business.includes(:reviewers).find(params[:id]) 64 | @users = @business.reviewers 65 | render :reviewers 66 | end 67 | 68 | private 69 | 70 | def business_params 71 | params.require(:business).permit(:name, :address, :phone_number, :cuisine, 72 | :mon_start_time, :mon_end_time, :tue_start_time, :tue_end_time, :wed_start_time, 73 | :wed_end_time, :thur_start_time, :thur_end_time, :fri_start_time, :fri_end_time, 74 | :sat_start_time, :sat_end_time, :sun_start_time, :sun_end_time, :website, :image, :price_range, 75 | :average_rating, :review_count, :delivery, :noise_level, :lat, :lng, :bounds, :in_bounds) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /frontend/components/photos/photo_show.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class PhotoShow extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.pageReturn = this.pageReturn.bind(this); 8 | this.nextPhoto = this.nextPhoto.bind(this); 9 | this.lastPhoto = this.lastPhoto.bind(this); 10 | this.photoNext = this.photoNext.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | document.querySelector("html").setAttribute("style", "overflow: hidden;"); 15 | } 16 | 17 | componentWillUnmount() { 18 | document.querySelector("html").setAttribute("style", "overflow: auto;"); 19 | } 20 | 21 | photoNext(e) { 22 | if (e.nativeEvent.offsetX < 150) { 23 | this.lastPhoto(); 24 | } else { 25 | this.nextPhoto(); 26 | } 27 | } 28 | 29 | nextPhoto() { 30 | let next = (this.props.photoNumber + 1) % this.props.photos.length; 31 | this.props.history.push( 32 | "/businesses/" + 33 | this.props.match.params.businessId + 34 | "/photos/" + 35 | this.props.photos[next].id 36 | ); 37 | } 38 | 39 | lastPhoto() { 40 | let last = this.props.photoNumber - 1; 41 | if (last < 0) { 42 | last = last + this.props.photos.length; 43 | } 44 | this.props.history.push( 45 | "/businesses/" + 46 | this.props.match.params.businessId + 47 | "/photos/" + 48 | this.props.photos[last].id 49 | ); 50 | } 51 | 52 | pageReturn(e) { 53 | if (e._targetInst.memoizedProps.value) { 54 | this.props.history.push( 55 | "/businesses/" + this.props.match.params.businessId + "/photos" 56 | ); 57 | } 58 | } 59 | 60 | render() { 61 | if (this.props.photo === undefined) { 62 | return null; 63 | } else { 64 | return ( 65 |
    66 |
    67 |
    68 | Close 69 |
    70 |
    71 |
    72 |
    73 | 74 |
    75 |
    76 | 81 |
    82 | {this.props.photoNumber + 1} of {this.props.photos.length} 83 |
    84 |
    85 |
    86 | 87 |
    88 |
    89 |
    90 | ); 91 | } 92 | } 93 | } 94 | 95 | export default PhotoShow; 96 | -------------------------------------------------------------------------------- /frontend/components/reviews/review_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { 3 | clearErrors, 4 | clearPage, 5 | nextPage, 6 | logout 7 | } from "../../actions/session_actions"; 8 | import { 9 | createReview, 10 | updateReview, 11 | fetchReview 12 | } from "../../actions/review_actions"; 13 | import { fetchBusiness, fetchBusinesses } from "../../actions/business_actions"; 14 | import { withRouter } from "react-router-dom"; 15 | import ReviewForm from "./review_form"; 16 | import { clearFilter } from "../../actions/filter_actions"; 17 | 18 | const mapStateToProps = (state, ownProps) => { 19 | if (ownProps.match.path === "/businesses/:businessId/reviews/new") { 20 | let review = { 21 | rating: "", 22 | body: "", 23 | price_range: "", 24 | noise_level: "", 25 | delivery: "", 26 | business_id: ownProps.match.params.businessId, 27 | user_id: state.session.currentUser.id 28 | }; 29 | 30 | return { 31 | formType: "new", 32 | review, 33 | errors: state.errors.session, 34 | business: state.business[ownProps.match.params.businessId], 35 | currentUser: state.session.currentUser, 36 | intendedPage: state.intendedPage 37 | }; 38 | } else { 39 | let reviewToUse = state.reviews[ownProps.match.params.reviewId]; 40 | let review = {}; 41 | if (reviewToUse) { 42 | for (var key in reviewToUse) { 43 | if (reviewToUse[key] === null) { 44 | review[key] = ""; 45 | } else { 46 | review[key] = reviewToUse[key]; 47 | } 48 | } 49 | } 50 | return { 51 | formType: "edit", 52 | review, 53 | business: state.business[ownProps.match.params.businessId], 54 | currentUser: state.session.currentUser, 55 | errors: state.errors.session, 56 | intendedPage: state.intendedPage 57 | }; 58 | } 59 | }; 60 | 61 | const mapDispatchToProps = (dispatch, ownProps) => { 62 | if (ownProps.match.path === "/businesses/:businessId/reviews/new") { 63 | return { 64 | createReview: review => dispatch(createReview(review)), 65 | clearErrors: () => dispatch(clearErrors()), 66 | clearPage: () => dispatch(clearPage()), 67 | fetchBusiness: businessId => dispatch(fetchBusiness(businessId)), 68 | logout: () => dispatch(logout()), 69 | nextPage: page => dispatch(nextPage(page)), 70 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)), 71 | clearFilter: () => dispatch(clearFilter()) 72 | }; 73 | } else { 74 | return { 75 | fetchReview: reviewId => dispatch(fetchReview(reviewId)), 76 | updateReview: review => dispatch(updateReview(review)), 77 | clearErrors: () => dispatch(clearErrors()), 78 | clearPage: () => dispatch(clearPage()), 79 | fetchBusiness: businessId => dispatch(fetchBusiness(businessId)), 80 | logout: () => dispatch(logout()), 81 | nextPage: page => dispatch(nextPage(page)), 82 | fetchBusinesses: filters => dispatch(fetchBusinesses(filters)), 83 | clearFilter: () => dispatch(clearFilter()) 84 | }; 85 | } 86 | }; 87 | 88 | export default withRouter( 89 | connect(mapStateToProps, mapDispatchToProps)(ReviewForm) 90 | ); 91 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 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: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: Help_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: Help 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: Help_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: Help_production 84 | username: Help 85 | password: <%= ENV['HELP_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20171102183657) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "businesses", force: :cascade do |t| 19 | t.string "name", null: false 20 | t.string "address", null: false 21 | t.string "phone_number", null: false 22 | t.string "cuisine", null: false 23 | t.string "website" 24 | t.integer "mon_start_time" 25 | t.integer "mon_end_time" 26 | t.integer "tue_start_time" 27 | t.integer "tue_end_time" 28 | t.integer "wed_start_time" 29 | t.integer "wed_end_time" 30 | t.integer "thur_start_time" 31 | t.integer "thur_end_time" 32 | t.integer "fri_start_time" 33 | t.integer "fri_end_time" 34 | t.integer "sat_start_time" 35 | t.integer "sat_end_time" 36 | t.integer "sun_start_time" 37 | t.integer "sun_end_time" 38 | t.datetime "created_at", null: false 39 | t.datetime "updated_at", null: false 40 | t.string "image_file_name" 41 | t.string "image_content_type" 42 | t.integer "image_file_size" 43 | t.datetime "image_updated_at" 44 | t.float "lat" 45 | t.float "lng" 46 | end 47 | 48 | create_table "photos", force: :cascade do |t| 49 | t.integer "user_id", null: false 50 | t.integer "business_id", null: false 51 | t.datetime "created_at", null: false 52 | t.datetime "updated_at", null: false 53 | t.string "image_file_name" 54 | t.string "image_content_type" 55 | t.integer "image_file_size" 56 | t.datetime "image_updated_at" 57 | end 58 | 59 | create_table "reviews", force: :cascade do |t| 60 | t.integer "user_id", null: false 61 | t.integer "business_id", null: false 62 | t.integer "rating", null: false 63 | t.integer "price_range" 64 | t.text "body", null: false 65 | t.boolean "delivery" 66 | t.integer "noise_level" 67 | t.datetime "created_at", null: false 68 | t.datetime "updated_at", null: false 69 | end 70 | 71 | create_table "users", force: :cascade do |t| 72 | t.string "first_name", null: false 73 | t.string "last_name", null: false 74 | t.string "email", null: false 75 | t.string "password_digest", null: false 76 | t.string "session_token", null: false 77 | t.string "zip_code", null: false 78 | t.date "birthday" 79 | t.datetime "created_at", null: false 80 | t.datetime "updated_at", null: false 81 | t.string "image_file_name" 82 | t.string "image_content_type" 83 | t.integer "image_file_size" 84 | t.datetime "image_updated_at" 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /app/assets/stylesheets/businesses.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the businesses controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | 5 | .main-div { 6 | margin: 0 auto; 7 | justify-content: center; 8 | width: 100%; 9 | } 10 | 11 | .biz-idx-page { 12 | display: flex; 13 | justify-content: center; 14 | width: 100%; 15 | } 16 | 17 | .holding-box { 18 | max-width: 1100px; 19 | display: flex; 20 | margin: 0 auto; 21 | 22 | } 23 | .all-biz { 24 | display: flex; 25 | justify-content: space-between; 26 | width: 630px; 27 | margin-bottom: 12px; 28 | border-top: 1px solid #E6E6E6; 29 | margin: 0 auto; 30 | } 31 | 32 | .biz-item { 33 | margin-top: 17px; 34 | margin-left: 28px; 35 | font-family: sans-serif; 36 | margin-bottom: 40px; 37 | } 38 | 39 | .business-photo { 40 | height: 92px; 41 | width: 92px; 42 | border-radius: 5px; 43 | object-fit: cover; 44 | } 45 | 46 | 47 | 48 | .list-item { 49 | width: 100%; 50 | height: 120px; 51 | 52 | display: flex; 53 | 54 | } 55 | 56 | .list-item>li { 57 | margin-top: 20px; 58 | } 59 | 60 | .index-and-name { 61 | display: flex; 62 | margin-left: 12px; 63 | } 64 | 65 | .biz-name-link { 66 | color: #0073BB; 67 | margin-left: 5px; 68 | } 69 | .biz-name-link:hover { 70 | text-decoration: underline; 71 | } 72 | 73 | 74 | 75 | 76 | .business-info { 77 | font-size: 14px; 78 | width: 219px; 79 | color: #333333; 80 | } 81 | 82 | .biz-info-div { 83 | margin-top: 35px; 84 | 85 | } 86 | 87 | .index-and-name { 88 | margin-top: 5px; 89 | } 90 | 91 | 92 | .rating-bar-line { 93 | display: flex; 94 | margin-left: 12px; 95 | margin-top: 5px; 96 | } 97 | 98 | .price-cuisine { 99 | display: flex; 100 | margin-left: 12px; 101 | margin-top: 5px; 102 | } 103 | 104 | .period { 105 | color: #999; 106 | width: 5px; 107 | margin-bottom: 7px; 108 | margin-left: 4px; 109 | padding-right: 10px; 110 | } 111 | 112 | 113 | .period-container { 114 | width: 5px; 115 | margin-bottom: 7px; 116 | } 117 | 118 | .cuisine { 119 | margin-left: 8px; 120 | font-family: sans-serif; 121 | font-size: 14px; 122 | color: #0073BB; 123 | } 124 | 125 | .cuisine:hover { 126 | text-decoration: underline; 127 | } 128 | 129 | .biz-review-count-index { 130 | color: #666666; 131 | font-size: 14px; 132 | font-family: sans-serif; 133 | margin-left: 4px; 134 | margin-right: 4px; 135 | margin-top: 5px; 136 | } 137 | 138 | .biz-review-text-index { 139 | margin-top: 5px; 140 | color: #666666; 141 | font-size: 14px; 142 | font-family: sans-serif; 143 | margin-left: 4px; 144 | margin-right: 4px; 145 | } 146 | 147 | .price-range-undefined { 148 | color: #666666; 149 | font-size: 12px; 150 | font-family: sans-serif; 151 | margin-left: 4px; 152 | margin-right: 4px; 153 | } 154 | 155 | .write-the-first { 156 | color: #0073BB; 157 | font-size: 13px; 158 | font-family: sans-serif; 159 | margin-left: 4px; 160 | margin-right: 4px; 161 | margin-bottom:0px; 162 | padding-top: 10px 163 | } 164 | 165 | .write-the-first:hover { 166 | text-decoration: underline; 167 | } 168 | 169 | 170 | .write-review-index-item { 171 | padding-top: 3px; 172 | } 173 | 174 | 175 | .no-biz { 176 | font-weight: bold; 177 | font-size: 24px; 178 | margin-top: 200px; 179 | margin-right: 100px; 180 | } 181 | 182 | 183 | 184 | // 185 | -------------------------------------------------------------------------------- /frontend/components/reviews/review_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import dateFormat from "dateformat"; 4 | import zipcodes from 'zipcodes'; 5 | 6 | class ReviewIndexItem extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.handleRating = this.handleRating.bind(this); 10 | this.editLink = this.editLink.bind(this); 11 | } 12 | 13 | handleRating() { 14 | let picture = [ 15 | "", 16 | "https://s3.amazonaws.com/helpcoreyladovskyprojectdev/small-ratings/one_star_review.png", 17 | "https://s3.amazonaws.com/helpcoreyladovskyprojectdev/small-ratings/two_star_review.png", 18 | "https://s3.amazonaws.com/helpcoreyladovskyprojectdev/small-ratings/three_star_review.png", 19 | "https://s3.amazonaws.com/helpcoreyladovskyprojectdev/small-ratings/four_star_review.png", 20 | "https://s3.amazonaws.com/helpcoreyladovskyprojectdev/small-ratings/five_star_review.png" 21 | ]; 22 | let rating = this.props.review.rating; 23 | let pic = picture[rating]; 24 | return ; 25 | } 26 | 27 | editLink() { 28 | if (this.props.currentUser === null) { 29 | return null; 30 | } else if (this.props.user.id === this.props.currentUser.id) { 31 | return ( 32 |
      33 |
    • 34 | 40 | Edit Review 41 | 42 |
    • 43 |
    • 44 | 50 |
    • 51 |
    52 | ); 53 | } 54 | } 55 | 56 | render() { 57 | if (this.props.user === undefined) { 58 | return null; 59 | } else { 60 | const date = dateFormat(this.props.review.created_at, "mm/dd/yyyy"); 61 | const city = zipcodes.lookup(this.props.user.zip_code); 62 | return ( 63 |
    64 |
      65 |
    • 66 |
        67 |
      • 68 |
          69 |
        • 70 | 74 |
        • 75 |
        • 76 | {this.props.user.first_name} 77 |
        • 78 |
        • 79 | {this.props.user.last_name.substring(0, 1) + "."} 80 |
        • 81 |
        82 |
      • 83 | 84 |
      • {this.props.user.zip_code}
      • 85 |
      86 |
    • 87 |
    • 88 |
        89 |
          90 |
        • 91 | {this.handleRating()} 92 |
        • 93 |
        • {date}
        • 94 |
        95 |
      • 96 | {this.props.review.body} 97 |
      • 98 | {this.editLink()} 99 |
      100 |
    • 101 |
    102 |
    103 | ); 104 | } 105 | } 106 | } 107 | 108 | export default ReviewIndexItem; 109 | -------------------------------------------------------------------------------- /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 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`. 18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or 19 | # `config/secrets.yml.key`. 20 | config.read_encrypted_secrets = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | # Compress JavaScripts and CSS. 27 | config.assets.js_compressor = :uglifier 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.action_controller.asset_host = 'http://assets.example.com' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 41 | 42 | # Mount Action Cable outside main process or domain 43 | # config.action_cable.mount_path = nil 44 | # config.action_cable.url = 'wss://example.com/cable' 45 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 46 | 47 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 48 | # config.force_ssl = true 49 | 50 | # Use the lowest log level to ensure availability of diagnostic information 51 | # when problems arise. 52 | config.log_level = :debug 53 | 54 | # Prepend all log lines with the following tags. 55 | config.log_tags = [ :request_id ] 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Use a real queuing backend for Active Job (and separate queues per environment) 61 | # config.active_job.queue_adapter = :resque 62 | # config.active_job.queue_name_prefix = "Help_#{Rails.env}" 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | end 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HELP 2 | 3 | Hey there and welcome! Thanks for checking out Help. Help is a yelp like application that allows users to rate and review restaurants. Navigating the site is literally as easy as clicking a button. You're able to Sign up, log in (or demo login) and get straight to looking for delicious places to eat. From the business index page you can see the rating of each restaurant as well as it's price ranges. You are able to filter restaurants based off name, category, price, delivery, noise level, and location. Everything besides the restaurants core composition, are qualities derived from user scored averages from the reviews they write. A user can add, edit, and delete reviews (the stars work identical to yelps), as well as upload photos for a business. If you click on a business you can learn all about it. You can see past reviews, find it on the map, and look at their photos. From the photos page you can click on a picture to get a closer look. Feel free to click through and see all the photos associated with a business. 4 | 5 | Check out the site! [Help: it's where the food's at!](https://www.corey-ladovsky-help.club/#/) 6 | 7 | ### Technologies 8 | 9 | This app is a fullstack application that has been designed using Ruby on Rails in the backend and React Redux in the front end. 10 | In addition to using Postgress to hold my core database, I used Amazon Web Service as a second database in order to house pictures from the web. I am attaching these pictures with the paperclip gem. In order to do that I also had to install figaro so that I could make a gitignored yml file to hide my AWS keys. 11 | Using Ruby on Rails I was able to build models with proper validations, and controllers to handle my ajax requests. I am rendering JSON and using jbuilder files to pass information around my application. 12 | In the frontend I am using React in order to keep my App a single page application and Redux in order to store slices of state. This allows me to have persisting users as well as to aid my application dynamically. My application was designed using SCSS. 13 | 14 | ### Problem Solving 15 | 16 | After building my Auth Routes I realized that I had made all successful log ins/ sign ups go directly to the home/ landing page. This tends to be the norm but I realized that it posed a future problem that I would soon encounter. The problem was that most of my site is navigable before officially signing in. This is a problem because a user could potentially go to the page, check out the businesses, look at a specific business, decide to write a review and then be asked to log in. If I had left everything to the 'standard' setup my user would sign in, land back at the landing page and then have to find their way back to the original desired business they wished to write a review for. This would be a very bad and frustrating user experience. 17 | In order to get around this problem I decided to chisel out another slice of state. This slice is called intended page. Whenever I have a protected route that would otherwise send the user directly to sign in, I add a click handler that stores my intended page. Now, after they have a successful log in the landing page checks for the existence of an intended page. If one exists, the user is redirected back to the page they initially wished to visit. 18 | I am proud of the details that I paid attention to. I enjoyed manipulating the html with css. One of my initial difficulties was figuring out how to make the partial grey vertical partition in the center of the search bar. I was successful by creating divs inside of divs and camouflaging their background colors. The small div is propped in the center of the larger one and has a side border. My logo was also a challenge to create solely using css. I was successful by creating a text shadow to all different sides. 19 | ![Logo and part of search bar](https://github.com/coreyladovsky/help/blob/master/app/assets/images/logo_and_bar.png) 20 | 21 | Please feel free to reach out to me with any questions or request. Have a wonderful day! Enjoy Help! 22 | 23 | 24 | Things I plan to implement in the near future: I'd love to add an 'open now' filter. 25 | I'd also like to make my app responsive to different screen sizes. 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/navbar.scss: -------------------------------------------------------------------------------- 1 | 2 | .help-nav { 3 | display: inline-block; 4 | font-family: sans-serif; 5 | font-size: 27px; 6 | font-weight: 600; 7 | color: black; 8 | text-shadow: -2px -2px 0 white, 2px -2px 0 white, 9 | -2px 2px 0 white, 2px 2px 0 white; 10 | 11 | margin-top: 15px; 12 | 13 | 14 | } 15 | 16 | 17 | .red-nav-bar { 18 | background-color: #D32323; 19 | height:64px; 20 | justify-content: center; 21 | display: flex; 22 | margin: auto; 23 | padding-left: 20px; 24 | 25 | 26 | 27 | } 28 | 29 | .top-nav-search { 30 | display: flex; 31 | width: 100%; 32 | // width: 1100px; 33 | justify-content: center; 34 | // position: sticky; 35 | left: 28px; 36 | } 37 | 38 | .search-form-nav { 39 | display: flex; 40 | margin-top: 13px; 41 | margin-left: 25px; 42 | } 43 | 44 | .find-text-nav { 45 | font-family: sans-serif; 46 | font-weight: bold; 47 | background-color: white; 48 | padding-top: 9px; 49 | padding-left: 8px; 50 | border-radius: 4px 0px 0px 4px; 51 | height: 27px; 52 | } 53 | 54 | .nav-input-find { 55 | background-color: white; 56 | padding-left: 7px; 57 | height: 36px; 58 | width: 300px; 59 | font-family: sans-serif; 60 | } 61 | 62 | .near-text-nav { 63 | font-family: sans-serif; 64 | font-weight: bold; 65 | background-color: white; 66 | padding-top: 9px; 67 | padding-left: 8px; 68 | height: 27px; 69 | 70 | } 71 | 72 | .near-input-nav { 73 | background-color: white; 74 | height: 36px; 75 | padding-left: 7px; 76 | width: 290px; 77 | font-family: sans-serif; 78 | 79 | } 80 | 81 | .mag { 82 | background-color: #BD1F1F; 83 | height: 36px; 84 | width: 54px; 85 | color: white; 86 | border-radius: 0px 4px 4px 0px; 87 | } 88 | 89 | .fa-search { 90 | height: 15px; 91 | width: 15px; 92 | color: white; 93 | margin-left: 17px; 94 | } 95 | 96 | .mag:hover { 97 | background-color: #A71C1C; 98 | } 99 | 100 | .mag-but { 101 | height: 30px; 102 | width: 35px; 103 | margin-left: 8px; 104 | } 105 | 106 | .mag-but:hover { 107 | 108 | } 109 | 110 | .nav-sign-up { 111 | background-color: #BD1F1F; 112 | padding-left: 30px; 113 | padding-right: 30px; 114 | padding-bottom: 10px; 115 | padding-top: 10px; 116 | margin-left: 20px; 117 | border-radius: 4px; 118 | color: white; 119 | font-family: sans-serif; 120 | font-weight: bold; 121 | font-size: 14px; 122 | } 123 | .nav-sign-up:hover { 124 | background-color: #A71C1C; 125 | } 126 | 127 | .nav-sign-up-li { 128 | padding-top: 23px; 129 | } 130 | 131 | .nav-log-out { 132 | background-color: #BD1F1F; 133 | padding-left: 30px; 134 | padding-right: 30px; 135 | padding-bottom: 10px; 136 | padding-top: 10px; 137 | margin-left: 20px; 138 | border-radius: 4px; 139 | color: white; 140 | font-family: sans-serif; 141 | font-weight: bold; 142 | font-size: 14px; 143 | margin-top: -9px; 144 | } 145 | 146 | .nav-log-out:hover { 147 | background-color: #A71C1C; 148 | } 149 | 150 | .seperator { 151 | background-color: white; 152 | width: 2px; 153 | height: 36px; 154 | border-left: #CCCCCC; 155 | background-color: white; 156 | } 157 | 158 | .seperator-div { 159 | height: 21px; 160 | background-color: white; 161 | border-left: 1px solid #CCCCCC; 162 | margin-top: 7px; 163 | } 164 | 165 | .container { 166 | display: flex; 167 | flex-direction: column; 168 | // align-items: center; 169 | background-color: #D32323; 170 | margin: 0 auto; 171 | 172 | } 173 | .long-bar { 174 | width: 100%; 175 | height: 28px; 176 | background-color: #BD1F1F; 177 | display: flex; 178 | justify-content: space-around; 179 | 180 | } 181 | 182 | .thin-red-nav-bar { 183 | display: flex; 184 | color: white; 185 | font-family: sans-serif; 186 | font-size: 12px; 187 | font-weight: bold; 188 | background-color: #BD1F1F; 189 | width: 907px; 190 | height: 28px; 191 | justify-content: space-between; 192 | margin: auto; 193 | padding-left: 20px; 194 | 195 | } 196 | 197 | .review-nav:last-child:hover { 198 | background-color: #A71C1C; 199 | } 200 | 201 | .review-nav { 202 | width: 100px; 203 | margin-bottom: 2px; 204 | padding-top: 6px; 205 | padding-left: 15px; 206 | } 207 | 208 | .home-nav { 209 | width: 112px; 210 | margin-left: 25px; 211 | } 212 | 213 | .dark-nav-thin { 214 | width:870px; 215 | } 216 | 217 | 218 | 219 | 220 | // 221 | -------------------------------------------------------------------------------- /app/assets/stylesheets/photo_upload.scss: -------------------------------------------------------------------------------- 1 | .photo-upload-container { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 0 auto; 5 | } 6 | 7 | .biz-heading-photos { 8 | display: flex; 9 | margin: auto; 10 | margin-top: 18px; 11 | flex-direction: column; 12 | padding-left: 20px; 13 | width: 400px; 14 | 15 | } 16 | 17 | .photo-page-ul { 18 | display: flex; 19 | } 20 | 21 | .biz-name-photo { 22 | font-size: 22px; 23 | font-family: sans-serif; 24 | font-weight: bold; 25 | color: #0F70BB; 26 | 27 | } 28 | 29 | .biz-name-photo:hover { 30 | text-decoration: underline; 31 | } 32 | 33 | .add-photo-text { 34 | font-size: 22px; 35 | font-family: sans-serif; 36 | font-weight: bold; 37 | color: #D00D25; 38 | margin-left: 5px; 39 | } 40 | 41 | .all-photos-tag { 42 | font-family: sans-serif; 43 | font-size: 14px; 44 | color: #0F70BB; 45 | margin-top: 6px; 46 | 47 | } 48 | 49 | .all-photos-tag:hover { 50 | text-decoration: underline; 51 | } 52 | // TEXT INSIDE OF PHOTO UPLOAD 53 | .upload-text { 54 | width: 600px; 55 | height: 400px; 56 | position: absolute; 57 | display: flex; 58 | justify-content: center; 59 | 60 | } 61 | 62 | .upload-text h1 { 63 | font-family: sans-serif; 64 | font-weight: bold; 65 | color: #333333; 66 | font-size: 29px; 67 | margin-top: 80px; 68 | } 69 | 70 | .center-line { 71 | border-top: 1px solid #B2B2B2; 72 | margin-top: .5em; 73 | width: 200px; 74 | } 75 | 76 | .ok-text { 77 | color: #666674; 78 | } 79 | 80 | .ok-partition { 81 | display: flex; 82 | justify-content: space-around; 83 | margin-top: 50px; 84 | } 85 | 86 | .browse-files { 87 | display: flex; 88 | justify-content: center; 89 | margin-top: 55px; 90 | } 91 | 92 | .browse-files-button { 93 | height: 15px; 94 | width: 127px; 95 | background-color: #D70306; 96 | color: white; 97 | font-size: 15px; 98 | font-family: sans-serif; 99 | font-weight: bold; 100 | border: 1px solid #8D0005; 101 | border-radius: 5px; 102 | text-align: center; 103 | z-index: 2; 104 | padding-top: 10px; 105 | padding-bottom: 13px; 106 | } 107 | 108 | .second-upload { 109 | opacity: 0; 110 | height: 40px; 111 | width: 127px; 112 | left: 39.2%; 113 | bottom: 30.5%; 114 | position: absolute; 115 | } 116 | .second-upload:hover { 117 | cursor: pointer; 118 | } 119 | 120 | input[type="file"]:hover { 121 | opacity: 0; 122 | } 123 | 124 | .browse-files-button:hover { 125 | background-color: #E02E33; 126 | 127 | } 128 | 129 | .upload-text div { 130 | 131 | } 132 | 133 | 134 | //
    135 | //

    Drag and drop your photos here

    136 | //
    OR
    137 | // 138 | //
    139 | 140 | 141 | 142 | .image-upload { 143 | display: flex; 144 | margin: 0 auto; 145 | margin-top: 60px; 146 | width: 600px; 147 | height: 400px; 148 | border-radius: 5px; 149 | border: 3px dashed #B2B2B2; 150 | 151 | 152 | } 153 | 154 | .image-uploaded { 155 | z-index: 1; 156 | width: 1000000px; 157 | height: 400px; 158 | opacity: 0; 159 | } 160 | 161 | 162 | .image-preview { 163 | margin: 0px; 164 | width: 600px; 165 | height: 400px; 166 | position: absolute; 167 | object-fit: cover; 168 | } 169 | 170 | .spin-container { 171 | width: 600px; 172 | height: 400px; 173 | position: absolute; 174 | background-color:black; 175 | opacity: .5; 176 | color: white; 177 | font-size: 40px; 178 | z-index: 2; 179 | display: flex; 180 | justify-content: center; 181 | } 182 | 183 | .small-spin { 184 | display: flex; 185 | justify-content: center; 186 | height: 120px; 187 | width: 120px; 188 | margin-top: 110px; 189 | } 190 | 191 | .photo-form { 192 | display: flex; 193 | justify-content: center; 194 | flex-direction: column; 195 | margin-bottom: 40px; 196 | } 197 | 198 | .photo-submit-button { 199 | align-self: center; 200 | margin-left: 400px; 201 | margin-top: 20px; 202 | height: 15px; 203 | width: 127px; 204 | background-color: #D70306; 205 | color: white; 206 | font-size: 15px; 207 | font-family: sans-serif; 208 | font-weight: bold; 209 | border: 1px solid #8D0005; 210 | border-radius: 5px; 211 | text-align: center; 212 | z-index: 2; 213 | padding-top: 8px; 214 | padding-bottom: 10px; 215 | } 216 | .photo-submit-button:hover { 217 | background-color: #E02E33; 218 | } 219 | -------------------------------------------------------------------------------- /frontend/components/business/biz_landing_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, withRouter } from "react-router-dom"; 3 | import parser from "parse-address"; 4 | 5 | class BizLandingIndexItem extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.findRating = this.findRating.bind(this); 9 | } 10 | 11 | findRating(rating) { 12 | switch (rating) { 13 | case 5: 14 | return ( 15 | 20 | ); 21 | case 4.5: 22 | return ( 23 | 28 | ); 29 | case 4: 30 | return ( 31 | 36 | ); 37 | case 3.5: 38 | return ( 39 | 44 | ); 45 | case 3: 46 | return ( 47 | 52 | ); 53 | case 2.5: 54 | return ( 55 | 60 | ); 61 | case 2: 62 | return ( 63 | 68 | ); 69 | case 1.5: 70 | return ( 71 | 76 | ); 77 | case 1: 78 | return ( 79 | 84 | ); 85 | default: 86 | return ( 87 |
    88 | 93 | {" "} 94 | Write The First Review! 95 | 96 |
    97 | ); 98 | } 99 | } 100 | 101 | render() { 102 | let parsed_address = parser.parseLocation(this.props.business.address); 103 | return ( 104 |
  • 105 | 106 | 110 | 111 | 112 | 116 | {this.props.business.name} 117 | 118 | 119 |
      120 |
    • {this.findRating(this.props.business.average_rating)}
    • 121 |
    • 122 | {this.props.business.review_count} 123 |
    • 124 |
    • 125 | {this.props.business.review_count === 1 126 | ? " review" 127 | : " reviews"} 128 |
    • 129 |
    130 |
    131 |
    {this.props.business.cuisine}
    132 |
    133 | {parsed_address.city + ", " + parsed_address.state} 134 |
    135 |
    136 |
  • 137 | ); 138 | } 139 | } 140 | 141 | export default BizLandingIndexItem; 142 | -------------------------------------------------------------------------------- /app/assets/images/french.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | > 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | image/svg+xml 32 | 33 | 34 | 35 | 36 | Openclipart 37 | 38 | 39 | nc 40 | 2011-08-21T12:01:27 41 | country flag buttons (with ISO-3166-1 naming convention) 42 | http://openclipart.org/detail/156871/nc-by-koppi 43 | 44 | 45 | koppi 46 | 47 | 48 | 49 | 50 | ISO3166-1 51 | button 52 | clip art 53 | clipart 54 | country 55 | flag 56 | flags 57 | squared 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/assets/images/italy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | > 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | image/svg+xml 32 | 33 | 34 | 35 | 36 | Openclipart 37 | 38 | 39 | mx 40 | 2011-08-21T12:01:26 41 | country flag buttons (with ISO-3166-1 naming convention) 42 | http://openclipart.org/detail/156847/mx-by-koppi 43 | 44 | 45 | koppi 46 | 47 | 48 | 49 | 50 | ISO3166-1 51 | button 52 | clip art 53 | clipart 54 | country 55 | flag 56 | flags 57 | squared 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/assets/stylesheets/session_form.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .top-red-bar-with-star { 4 | background-color: #d32323; 5 | width: 100%; 6 | height: 64px; 7 | } 8 | 9 | .help { 10 | display: inline-block; 11 | align: center; 12 | font-family: sans-serif; 13 | font-size: 27px; 14 | font-weight: 600; 15 | color: black; 16 | text-shadow: -2px -2px 0 white, 2px -2px 0 white, 17 | -2px 2px 0 white, 2px 2px 0 white; 18 | margin-left: 48%; 19 | margin-top: 15px; 20 | } 21 | 22 | .help-div { 23 | display: inline; 24 | margin: 0 auto; 25 | align: center; 26 | } 27 | 28 | .login-signup-string { 29 | color: #d32323; 30 | font-size: 22px; 31 | font-weight: bold; 32 | font-family: sans-serif; 33 | margin-left: 200px; 34 | margin-top: 50px; 35 | } 36 | 37 | .little-message { 38 | font-size: 18px; 39 | font-weight: bold; 40 | font-family: sans-serif; 41 | margin-top: 7px; 42 | margin-left: 135px; 43 | } 44 | 45 | .login-message { 46 | display: flex; 47 | } 48 | 49 | .new-to-help { 50 | font-size: 15px; 51 | font-weight: bold; 52 | font-family: sans-serif; 53 | margin-top: 7px; 54 | margin-left: 210px; 55 | } 56 | 57 | .sign-up-link { 58 | font-size: 15px; 59 | margin-left: 10px; 60 | margin-top: 8px; 61 | font-weight: bold; 62 | font-family: sans-serif; 63 | color: #0066CC; 64 | } 65 | 66 | .sign-up-link:hover { 67 | text-decoration: underline; 68 | } 69 | 70 | .auth-form { 71 | // border: 1px solid black; 72 | width: 350px; 73 | margin-top: 50px; 74 | margin-left: 110px; 75 | } 76 | 77 | .auth-list input { 78 | border: 1px solid gray; 79 | height: 30px; 80 | font-family: sans-serif; 81 | border-radius: 4px; 82 | padding-left: 5px; 83 | color: #999; 84 | font-size: 14px; 85 | 86 | } 87 | 88 | .auth-list input:focus { 89 | color: black; 90 | border: 1px solid #0066CC 91 | } 92 | 93 | .auth-list li { 94 | margin-top: 5px; 95 | margin-bottom: 5px; 96 | } 97 | 98 | .name-inputs { 99 | display: flex; 100 | justify-content: space-between; 101 | } 102 | .first-name { 103 | width: 165px; 104 | } 105 | 106 | .last-name { 107 | width: 165px; 108 | } 109 | 110 | .email { 111 | width: 345px; 112 | 113 | } 114 | 115 | .password { 116 | width: 345px; 117 | margin-top: 7px; 118 | 119 | } 120 | 121 | .zip-code { 122 | width: 345px; 123 | margin-top: 15px; 124 | } 125 | 126 | 127 | .all-drop { 128 | background-color: white; 129 | height: 30px; 130 | width: 110px; 131 | color: black; 132 | font-size: 14px; 133 | } 134 | 135 | 136 | .drop { 137 | display:flex; 138 | justify-content: space-between; 139 | 140 | } 141 | 142 | .birthday-text { 143 | display: flex; 144 | border:0px; 145 | } 146 | 147 | .birthday { 148 | font-weight: bold; 149 | font-family: sans-serif; 150 | font-size: 14px; 151 | border:0px; 152 | } 153 | 154 | .optional { 155 | color: #999; 156 | font-size: 12px; 157 | font-family: sans-serif; 158 | margin-left: 8px; 159 | margin-top: 5px; 160 | } 161 | 162 | .submit-button { 163 | width: 350px; 164 | background-color: #d32323; 165 | text-align: center; 166 | color: white; 167 | font-weight: bold; 168 | height: 35px; 169 | border-radius: 4px; 170 | font-family: sans-serif; 171 | font-size: 14px; 172 | margin-top: 5px; 173 | } 174 | 175 | .submit-button:hover { 176 | background-color: #E80000 ; 177 | border: 1px #8d0005 178 | } 179 | 180 | .guest-log-in { 181 | width: 200px; 182 | background-color: #d32323; 183 | text-align: center; 184 | color: white; 185 | font-weight: bold; 186 | height: 30px; 187 | border-radius: 4px; 188 | font-family: sans-serif; 189 | font-size: 14px; 190 | margin-top: 5px; 191 | margin-left: 185px; 192 | } 193 | 194 | .guest-log-in:hover { 195 | background-color: #E80000 ; 196 | border: 1px #8d0005 197 | } 198 | 199 | .closing { 200 | display: flex; 201 | margin-left: 250px; 202 | margin-top: 10px; 203 | } 204 | 205 | .closer { 206 | color: #999; 207 | font-size: 12px; 208 | font-family: sans-serif; 209 | } 210 | 211 | .log-link { 212 | color: #003399; 213 | font-size: 12px; 214 | font-family: sans-serif; 215 | margin-left: 5px; 216 | } 217 | .log-link:hover { 218 | text-decoration: underline; 219 | } 220 | 221 | .session-errors { 222 | display: flex; 223 | } 224 | .session-error { 225 | margin-top: 15px; 226 | margin-left: 15px; 227 | color: red; 228 | font-size: 12px; 229 | font-family: sans-serif; 230 | // text-decoration: underline; 231 | text-decoration-color: red; 232 | } 233 | 234 | // footer { 235 | // position: absolute; 236 | // bottom: 0; 237 | // height: 60px; 238 | // width: 100%; 239 | // background-color: #F5F5F5; 240 | // border-top: 1px solid lightgray; 241 | // } 242 | 243 | .git-link { 244 | position: absolute; 245 | color: blue; 246 | font-size: 12px; 247 | font-family: sans-serif; 248 | bottom: 0; 249 | right: 0; 250 | margin-bottom: 25px; 251 | margin-right: 20px; 252 | } 253 | 254 | .git-link:hover { 255 | text-decoration: underline; 256 | } 257 | 258 | .session-flex { 259 | display: flex; 260 | justify-content: space-around; 261 | } 262 | 263 | .welcome-image { 264 | height: 375px; 265 | width: 375px; 266 | margin-top: 30%; 267 | margin-right: 20%; 268 | } 269 | 270 | 271 | 272 | // 273 | -------------------------------------------------------------------------------- /frontend/components/photos/photo_upload.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NavBar from "../NavBar/NavBar"; 3 | import { Link } from "react-router-dom"; 4 | 5 | class PhotoUpload extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | imageFile: null, 10 | imageUrl: null, 11 | loading: false 12 | }; 13 | this.updateFile = this.updateFile.bind(this); 14 | this.handleSubmit = this.handleSubmit.bind(this); 15 | this.photoText = this.photoText.bind(this); 16 | this.submitPhoto = this.submitPhoto.bind(this); 17 | this.spinSpinner = this.spinSpinner.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | this.props.clearPage(); 22 | this.props.fetchBusiness(this.props.match.params.businessId); 23 | } 24 | 25 | photoText() { 26 | if (this.state.imageFile) { 27 | return ""; 28 | } else { 29 | return ( 30 |
    31 |
      32 |
    • 33 |

      Drag and drop your photos here

      34 |
    • 35 |
    • 36 |
      37 |
      OR
      38 |
      39 |
    • 40 |
    • 41 |
      42 | Browse Files 43 | 49 |
      50 |
    • 51 |
    52 |
    53 | ); 54 | } 55 | } 56 | 57 | spinSpinner() { 58 | if (this.state.loading) { 59 | return ( 60 |
    61 |
    62 | 63 |
    64 |
    65 | ); 66 | } 67 | } 68 | 69 | updateFile(e) { 70 | let file = e.currentTarget.files[0]; 71 | let fileReader = new FileReader(); 72 | fileReader.onloadend = () => { 73 | this.setState({ imageFile: file, imageUrl: fileReader.result }); 74 | }; 75 | if (file) { 76 | fileReader.readAsDataURL(file); 77 | } 78 | } 79 | 80 | submitPhoto() { 81 | if (!this.state.imageFile) { 82 | return ""; 83 | } else { 84 | return ( 85 | 88 | ); 89 | } 90 | } 91 | 92 | handleSubmit(e) { 93 | e.preventDefault(); 94 | let formData = new FormData(); 95 | formData.append("photo[image]", this.state.imageFile); 96 | formData.append("photo[user_id]", this.props.currentUser.id); 97 | formData.append("photo[business_id]", this.props.business.id); 98 | this.setState({ loading: true }); 99 | this.props.createPhoto(formData).then( 100 | res => { 101 | this.setState({ loading: false }); 102 | this.props.history.push(`/businesses/${this.props.business.id}/photos`); 103 | }, 104 | res => { 105 | this.setState({ loading: false }); 106 | } 107 | ); 108 | } 109 | render() { 110 | if (this.props.business === undefined) { 111 | return null; 112 | } else { 113 | return ( 114 |
    115 | 122 |
    123 |
    124 |
      125 |
    • 126 |
      127 | 128 | {this.props.business.name + ":"} 129 | 130 |
      131 |
      132 | 133 | View all photos 134 | 135 |
      136 |
    • 137 |
    • Add Photos
    • 138 |
    139 |
    140 | 141 |
    142 |
    143 | {this.photoText()} 144 | {this.spinSpinner()} 145 | 151 | 152 |
    153 | {this.submitPhoto()} 154 |
    155 |
    156 |
    157 | ); 158 | } 159 | } 160 | } 161 | 162 | export default PhotoUpload; 163 | -------------------------------------------------------------------------------- /app/assets/stylesheets/review_form.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .write-review-text-form { 4 | color: #D70306; 5 | font-size: 16px; 6 | font-family: sans-serif; 7 | font-weight: bold; 8 | border-bottom: 1px solid #E6E6E6; 9 | padding-bottom: 5px; 10 | } 11 | 12 | .review-container-write-and-show { 13 | width: 646px; 14 | margin-left: 28px; 15 | margin-top: 23px; 16 | margin-bottom: 50px; 17 | } 18 | 19 | 20 | .main-container-review-form { 21 | width: 100%; 22 | display: flex; 23 | margin: 0 auto; 24 | justify-content: center; 25 | } 26 | 27 | .biz-info-div { 28 | display: flex; 29 | } 30 | 31 | .business-photo-review-form{ 32 | height: 62px; 33 | width: 62px; 34 | border-radius: 5px; 35 | } 36 | 37 | .biz-name-link { 38 | color: #0073BB; 39 | font-family: sans-serif; 40 | font-weight: bold; 41 | font-size: 14px; 42 | margin-left: 10px; 43 | } 44 | 45 | .cuisine-link-review-form { 46 | font-family: sans-serif; 47 | color: #0073BB; 48 | font-size: 12px; 49 | margin-left: 12px; 50 | } 51 | 52 | .biz-address-review-forms { 53 | font-family: sans-serif; 54 | font-size: 12px; 55 | margin-left: 12px; 56 | } 57 | 58 | .stars-and-review-body { 59 | height: 250px; 60 | width: 630px; 61 | border: 1px solid #999999; 62 | border-radius: 4px; 63 | } 64 | 65 | .stars-and-review-body:focus-within { 66 | border: 1px solid #68A6D5; 67 | } 68 | 69 | .your-review-text { 70 | font-weight: bold; 71 | font-size: 14px; 72 | font-family: sans-serif; 73 | margin-top: 20px; 74 | padding-bottom: 7px; 75 | } 76 | 77 | 78 | .stars-ratings-form ~ .five { 79 | background-color: #D00D24; 80 | } 81 | 82 | .stars-review-biz-form { 83 | height: 50px; 84 | width: 600px; 85 | border-bottom: 1px solid #999999; 86 | margin-left: 14px; 87 | margin-top: 7px; 88 | 89 | } 90 | 91 | 92 | .yelp-stars { 93 | margin-top: 18px; 94 | display: flex; 95 | flex-direction: row-reverse; 96 | justify-content: flex-end; 97 | width: 0px; 98 | } 99 | 100 | 101 | 102 | .default { 103 | height: 32px; 104 | width: 32px; 105 | background-color: #CCCCCC; 106 | color: white; 107 | display: flex; 108 | justify-content: center; 109 | border-radius: 5px; 110 | font-size: 23px; 111 | cursor: pointer; 112 | border: 2px solid white; 113 | } 114 | 115 | .rating- { 116 | display: flex; 117 | font-family: sans-serif; 118 | font-size: 13px; 119 | margin-top: 10px; 120 | min-width: 200px; 121 | margin-left: 5px; 122 | } 123 | 124 | 125 | .pale-yellow { 126 | background-color: #F2BC82; 127 | } 128 | 129 | .yellow { 130 | background-color: #FDC116; 131 | } 132 | .orangy { 133 | background-color: #FF9151; 134 | } 135 | 136 | .pale-red { 137 | background-color: #F25A54; 138 | } 139 | 140 | .red { 141 | background-color: #D4202B; 142 | } 143 | 144 | 145 | // Yelp css inspired by Roko C. Buljan from stack overflow 146 | .yelp-stars>label:nth-child(1):hover, 147 | .yelp-stars>label:nth-child(1):hover ~ label{background-color: #D4202B;} 148 | .yelp-stars>label:nth-child(2):hover, 149 | .yelp-stars>label:nth-child(2):hover ~ label{background-color: #F25A54;} 150 | .yelp-stars>label:nth-child(3):hover, 151 | .yelp-stars>label:nth-child(3):hover ~ label{background-color: #FF9151;} 152 | .yelp-stars>label:nth-child(4):hover, 153 | .yelp-stars>label:nth-child(4):hover ~ label{background-color: #FDC116;} 154 | .yelp-stars>label:nth-child(5):hover, 155 | .yelp-stars>label:nth-child(5):hover ~ label{background-color: #F2BC82;} 156 | 157 | 158 | 159 | .stars-ratings-form { 160 | display: flex; 161 | flex-direction: column; 162 | 163 | } 164 | .stars-ratings-form ul { 165 | display: flex; 166 | } 167 | 168 | .rating-text-review { 169 | font-family: sans-serif; 170 | margin-left: 190px; 171 | margin-top: -25px; 172 | 173 | } 174 | 175 | 176 | 177 | 178 | .fa-star { 179 | font-size: 25px; 180 | height: 32px; 181 | width: 32px; 182 | margin-top: 4px; 183 | margin-left: 6px; 184 | } 185 | 186 | 187 | .text-review-form { 188 | margin-top: 10px; 189 | margin-left: 12px; 190 | width: 618px; 191 | height: 170px; 192 | font-family: sans-serif; 193 | } 194 | 195 | .add-info-form { 196 | color: #D70306; 197 | font-size: 16px; 198 | font-family: sans-serif; 199 | font-weight: bold; 200 | padding-bottom: 5px; 201 | margin-top: 15px; 202 | } 203 | 204 | .price-range-form { 205 | font-size: 14px; 206 | font-family: sans-serif; 207 | font-weight: bold; 208 | background-color: #F5F5F5; 209 | border-top: 1px solid #E6E6E6; 210 | padding-top: 15px; 211 | padding-left: 10px; 212 | } 213 | 214 | .price-list-form { 215 | display: flex; 216 | font-family: sans-serif; 217 | font-weight: 400; 218 | margin-top: 5px; 219 | margin-left: 15px; 220 | padding-bottom: 20px; 221 | padding-top: 20px; 222 | } 223 | 224 | .price-list-form li { 225 | margin-left: 15px; 226 | margin-right: 8px; 227 | } 228 | 229 | .delivery-form { 230 | background-color: #FFFFFF; 231 | font-size: 14px; 232 | font-family: sans-serif; 233 | font-weight: bold; 234 | border-top: 1px solid #E6E6E6; 235 | padding-top: 15px; 236 | padding-left: 10px; 237 | } 238 | 239 | .post-review-button { 240 | background-color: #E7000A; 241 | color: white; 242 | font-family: sans-serif; 243 | font-weight: bold; 244 | font-size: 12px; 245 | border: 1px solid #8B0006; 246 | width: 90px; 247 | height: 33px; 248 | border-radius: 4px; 249 | margin-top: 20px; 250 | padding-left: 24px; 251 | } 252 | 253 | .post-review-button:hover { 254 | background-color: #E02E33; 255 | } 256 | 257 | .cancel-review { 258 | color: #3D7CC4; 259 | font-family: sans-serif; 260 | font-size: 12px; 261 | margin-left: 15px; 262 | } 263 | 264 | .cancel-review:hover { 265 | text-decoration: underline; 266 | } 267 | 268 | .other-reviews { 269 | display: flex; 270 | justify-content: space-between; 271 | margin: auto; 272 | padding-left: 20px; 273 | } 274 | 275 | 276 | .main-div-form { 277 | display: flex; 278 | width: 100%; 279 | margin-top: 70px; 280 | } 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | // 312 | -------------------------------------------------------------------------------- /app/assets/stylesheets/business_show.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | .biz-show-head { 6 | display: flex; 7 | max-width: 1100px; 8 | margin: 0 auto; 9 | margin-top: 100px; 10 | padding-left: 30px; 11 | justify-content: space-between; 12 | min-width: 800px; 13 | } 14 | 15 | .biz-name-main { 16 | font-size: 30px; 17 | font-family: sans-serif; 18 | font-weight: bold; 19 | } 20 | 21 | .rating-review-show { 22 | display: flex; 23 | margin-top: 19px; 24 | } 25 | 26 | .review-number-show { 27 | color: #666666; 28 | font-size: 14px; 29 | font-family: sans-serif; 30 | margin-left: 4px; 31 | margin-right: 4px; 32 | margin-top: 8px; 33 | } 34 | 35 | .review-text-show { 36 | color: #666666; 37 | font-size: 14px; 38 | font-family: sans-serif; 39 | margin-top: 8px; 40 | } 41 | 42 | .price-cuisine-show { 43 | display: flex; 44 | font-size: 18px; 45 | font-family: sans-serif; 46 | margin-top: 14px; 47 | } 48 | 49 | .cusine-link-show{ 50 | color: #0073BB; 51 | margin-left: 8px; 52 | } 53 | 54 | .cusine-link-show:hover { 55 | text-decoration: underline; 56 | } 57 | 58 | .add-buttons-show { 59 | display: flex; 60 | margin-top: 65px; 61 | justify-content: center; 62 | } 63 | 64 | .review-button-show { 65 | height: 36px; 66 | width: 176px; 67 | background-color: #D70306; 68 | color: white; 69 | font-size: 15px; 70 | font-family: sans-serif; 71 | font-weight: bold; 72 | border: 1px solid #8D0005; 73 | border-radius: 5px; 74 | padding-top: 10px; 75 | padding-bottom: 10px; 76 | padding-left: 15px; 77 | padding-right: 17px; 78 | margin-left: 5px; 79 | 80 | 81 | } 82 | 83 | .fa-star:before { 84 | font-size: 20px; 85 | } 86 | 87 | .review-button-show:hover { 88 | background-color: #E02E33; 89 | } 90 | 91 | .review-button-button-show { 92 | height: 36px; 93 | width: 176; 94 | margin-left: 5px; 95 | font-size: 15px; 96 | padding-top: 5px; 97 | margin-right: 5px; 98 | } 99 | 100 | .fa-star { 101 | margin-left: 5px; 102 | font-size: 180px; 103 | height: 20px; 104 | width: 20px; 105 | } 106 | 107 | .photo-button-show { 108 | border: 1px solid #CCCCCC; 109 | color: #666666; 110 | height: 22px; 111 | width: 100px; 112 | font-size: 12px; 113 | font-weight: bold; 114 | font-family: sans-serif; 115 | display: flex; 116 | margin-left: 12px; 117 | margin-top: 4px; 118 | padding-top: 8px; 119 | padding-left: 18px; 120 | } 121 | 122 | .photo-button-show:hover { 123 | background-color: #F5F5F5; 124 | } 125 | 126 | 127 | .fa-camera { 128 | margin-right: 4px; 129 | display: none; 130 | } 131 | 132 | .website-link { 133 | color: #0073BB; 134 | font-family: sans-serif; 135 | font-size: 17px; 136 | margin-top: 7px; 137 | } 138 | 139 | .website-link:hover { 140 | text-decoration: underline; 141 | } 142 | 143 | .map-show { 144 | width: 300px; 145 | height: 400px; 146 | margin-top: 40px; 147 | 148 | } 149 | 150 | .picture-list-show { 151 | margin-top: 148px; 152 | display: flex; 153 | } 154 | 155 | .first-photo-show { 156 | height: 220px; 157 | width: 200px; 158 | border: 1px solid black; 159 | display: none; 160 | } 161 | .second-photo-show { 162 | height: 250px; 163 | width: 250px; 164 | margin-top: -10px; 165 | margin-right: 250px; 166 | } 167 | .third-photo-show { 168 | height: 220px; 169 | width: 200px; 170 | border: 1px solid black; 171 | display: none; 172 | } 173 | 174 | .biz-indiv-photo { 175 | height: 220px; 176 | width: 250px; 177 | object-fit: cover; 178 | 179 | } 180 | 181 | .photo-items-ul { 182 | display: flex; 183 | width: 750px; 184 | margin-top: 120px; 185 | 186 | } 187 | 188 | .biz-address-show { 189 | font-family: sans-serif; 190 | margin-top: 7px; 191 | } 192 | 193 | 194 | .photo-items-ul li { 195 | height: 250px; 196 | width: 250px; 197 | display: flex; 198 | align-items: center; 199 | } 200 | 201 | .photo-items-ul li:hover { 202 | cursor: pointer; 203 | } 204 | 205 | .all-hours-show { 206 | margin-bottom: 200px; 207 | margin-top: 28px; 208 | } 209 | 210 | .hours-div-show { 211 | color: #D70306; 212 | font-size: 18px; 213 | font-family: sans-serif; 214 | font-weight: bold; 215 | 216 | 217 | 218 | } 219 | 220 | .biz-show-reviews { 221 | 222 | } 223 | 224 | .biz-show-reviews-text { 225 | color: #D70306; 226 | font-size: 18px; 227 | font-family: sans-serif; 228 | font-weight: bold; 229 | margin-top: 30px; 230 | } 231 | 232 | 233 | 234 | 235 | .week-day { 236 | font-weight: bold; 237 | margin-right: 25px; 238 | } 239 | 240 | .hour-container-show { 241 | display: flex; 242 | font-family: sans-serif; 243 | font-size: 14px; 244 | 245 | } 246 | 247 | .hour-container-show ul li { 248 | margin-top: 5px; 249 | } 250 | 251 | 252 | .all-hours-show { 253 | border-left: 1px solid #E6E6E6; 254 | padding-left: 50px; 255 | padding-right: 50px; 256 | 257 | 258 | } 259 | 260 | 261 | .more-info-div { 262 | margin-top: 60px; 263 | } 264 | 265 | .noise-level-info-show { 266 | display: flex; 267 | } 268 | 269 | .delivery-info-show { 270 | display: flex; 271 | } 272 | 273 | .info-col-show { 274 | margin-top: 8px; 275 | font-size: 14px; 276 | font-family: sans-serif; 277 | 278 | } 279 | 280 | .info-col-ans-show { 281 | margin-top: 8px; 282 | font-size: 14px; 283 | font-family: sans-serif; 284 | font-weight: bold; 285 | margin-left: 7px; 286 | } 287 | 288 | .star-rating-show { 289 | display: flex; 290 | vertical-align: bottom; 291 | // padding-top: 8px; 292 | } 293 | 294 | .write-review-show-page-link { 295 | font-family: sans-serif; 296 | font-size: 13px; 297 | color: #5399CE; 298 | position: relative; 299 | align-self: baseline; 300 | } 301 | 302 | .review-div-show { 303 | padding-top: 6px; 304 | } 305 | 306 | .write-review-show-page-link:hover { 307 | text-decoration: underline; 308 | } 309 | 310 | .price-range-undefined { 311 | margin-top: 4px; 312 | } 313 | 314 | .see-all-photos { 315 | color:#0F70BB; 316 | display: flex; 317 | float: right; 318 | } 319 | 320 | .see-all-photos div { 321 | margin-left: 5px; 322 | } 323 | .see-all-photos div:hover { 324 | text-decoration: underline; 325 | } 326 | 327 | 328 | .fa-th-large { 329 | font-size: 10px; 330 | color: #666666; 331 | } 332 | 333 | 334 | 335 | 336 | // 337 | -------------------------------------------------------------------------------- /frontend/components/landing/landing.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import SessionFormContainer from "../session/session_form_container"; 4 | import SearchFormContainer from "../search_form_container"; 5 | import BizLandingIndex from "../business/business_landing_container"; 6 | 7 | class Landing extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.logButton = this.logButton.bind(this); 11 | this.clickHandler = this.clickHandler.bind(this); 12 | this.searchSubmit = this.searchSubmit.bind(this); 13 | this.cuisineSearch = this.cuisineSearch.bind(this); 14 | this.heroChange = this.heroChange.bind(this); 15 | this.classes = [ 16 | "landing-background1", 17 | "landing-background2", 18 | "landing-background3", 19 | "landing-background4", 20 | "landing-background5", 21 | "landing-background6", 22 | "landing-background7", 23 | "landing-background8", 24 | "landing-background9" 25 | ]; 26 | this.heroOff; 27 | } 28 | 29 | componentWillReceiveProps(nextprops) { 30 | if (this.props.formType !== nextprops.formType) { 31 | this.setState(); 32 | } 33 | } 34 | 35 | clickHandler(event) { 36 | this.props.nextPage("/reviews"); 37 | } 38 | 39 | componentDidMount() { 40 | if (this.props.intendedPage) { 41 | this.props.history.push(this.props.intendedPage); 42 | this.props.clearPage(); 43 | } else { 44 | this.props.fetchBusinesses({ 45 | cuisine: "", 46 | price_range: 4, 47 | noise_level: 4, 48 | delivery: false, 49 | bounds: "", 50 | name: "" 51 | }); 52 | this.heroChange(); 53 | } 54 | } 55 | 56 | componentWillUnmount() { 57 | clearInterval(this.heroOff); 58 | } 59 | 60 | searchSubmit() { 61 | this.props.fetchBusinesses({ 62 | cuisine: "", 63 | price_range: 4, 64 | noise_level: 4, 65 | delivery: false, 66 | bounds: "", 67 | name: "" 68 | }); 69 | this.props.history.push("/search"); 70 | } 71 | 72 | cuisineSearch(event) { 73 | this.props.fetchBusinesses({ 74 | cuisine: event._targetInst.memoizedProps.value.toLowerCase(), 75 | price_range: 4, 76 | noise_level: 4, 77 | delivery: false, 78 | bounds: "", 79 | name: event._targetInst.memoizedProps.value.toLowerCase() 80 | }); 81 | this.props.history.push("/search"); 82 | } 83 | 84 | logButton() { 85 | if (this.props.currentUser) { 86 | return ( 87 | 90 | ); 91 | } else { 92 | return ( 93 |
    94 |
      95 |
    • 96 | 97 | Log In 98 | 99 |
    • 100 |
    • 101 | 102 | Sign Up 103 | 104 |
    • 105 |
    106 |
    107 | ); 108 | } 109 | } 110 | 111 | 112 | heroChange() { 113 | this.heroOff = setInterval(() => { 114 | $("#landing-hero").removeClass(); 115 | let picClass = this.classes[ 116 | Math.floor(Math.random() * this.classes.length) 117 | ]; 118 | $("#landing-hero").addClass("landing-background " + picClass); 119 | }, 5000); 120 | } 121 | 122 | render() { 123 | let final = 124 | "landing-background " + 125 | this.classes[Math.floor(Math.random() * this.classes.length)]; 126 | return ( 127 |
    128 |
    129 |
    130 |
      131 |
    • 132 |
    • {this.logButton()}
    • 133 |
    134 |
    135 |
    136 |
    137 |
    138 | 139 | help 140 | 141 |
    142 | 146 |
    147 |