├── log
└── .keep
├── app
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── concerns
│ │ └── .keep
│ ├── user.rb
│ ├── booking.rb
│ └── api_client.rb
├── assets
│ ├── images
│ │ ├── .keep
│ │ └── auth-flow.gif
│ ├── javascripts
│ │ ├── components
│ │ │ ├── .gitkeep
│ │ │ ├── api_client.js
│ │ │ ├── has_timer.js.jsx
│ │ │ ├── has_drop_menu.js.jsx
│ │ │ ├── loading_overlay.js.jsx
│ │ │ ├── book_now_button.js.jsx
│ │ │ ├── new_booking_page.js.jsx
│ │ │ ├── primary_navigation.js.jsx
│ │ │ ├── edit_booking_page.js.jsx
│ │ │ ├── booking_pending_button.js.jsx
│ │ │ ├── booking_complete_button.js.jsx
│ │ │ ├── booking_form.js.jsx
│ │ │ ├── list_page.js.jsx
│ │ │ └── login_page.js.jsx
│ │ ├── components.js
│ │ └── application.js
│ └── stylesheets
│ │ ├── spinkit
│ │ ├── _variables.scss
│ │ ├── spinkit.scss
│ │ └── spinners
│ │ │ ├── 5-pulse.scss
│ │ │ ├── 1-rotating-plane.scss
│ │ │ ├── 9-wordpress.scss
│ │ │ ├── 2-double-bounce.scss
│ │ │ ├── 3-wave.scss
│ │ │ ├── 7-three-bounce.scss
│ │ │ ├── 6-chasing-dots.scss
│ │ │ ├── 4-wandering-cubes.scss
│ │ │ ├── 9-cube-grid.scss
│ │ │ ├── 8-circle.scss
│ │ │ └── 10-fading-circle.scss
│ │ └── application.css.scss
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── pages_controller.rb
│ ├── sessions_controller.rb
│ ├── locations_controller.rb
│ ├── users_controller.rb
│ ├── authentication_controller.rb
│ ├── application_controller.rb
│ └── bookings_controller.rb
├── helpers
│ └── application_helper.rb
├── views
│ ├── bookings
│ │ ├── new.html.erb
│ │ └── edit.html.erb
│ ├── pages
│ │ ├── home.html.erb
│ │ └── rentals.html.erb
│ ├── authentication
│ │ └── new.html.erb
│ └── layouts
│ │ └── application.html.erb
├── serializers
│ └── booking_serializer.rb
└── services
│ └── perform_bookings.rb
├── lib
├── assets
│ └── .keep
└── tasks
│ ├── .keep
│ └── perform_bookings.rake
├── public
├── favicon.ico
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── vendor
└── assets
│ ├── javascripts
│ └── .keep
│ └── stylesheets
│ └── .keep
├── README.md
├── Procfile
├── bin
├── rake
├── bundle
├── rails
└── setup
├── config
├── boot.rb
├── initializers
│ ├── twilio.rb
│ ├── geocoder.rb
│ ├── cookies_serializer.rb
│ ├── active_model_serializer.rb
│ ├── mime_types.rb
│ ├── filter_parameter_logging.rb
│ ├── session_store.rb
│ ├── backtrace_silencers.rb
│ ├── assets.rb
│ ├── wrap_parameters.rb
│ └── inflections.rb
├── environment.rb
├── unicorn.rb
├── locales
│ └── en.yml
├── secrets.yml
├── environments
│ ├── development.rb
│ ├── test.rb
│ └── production.rb
├── application.rb
├── routes.rb
└── database.yml
├── config.ru
├── db
├── migrate
│ ├── 20150322223317_add_password_digest_to_users.rb
│ ├── 20150324011147_add_car_plate_to_bookings.rb
│ ├── 20150324235103_add_asap_to_bookings.rb
│ ├── 20150325034141_add_reservation_id_to_bookings.rb
│ ├── 20150325034449_add_reservation_response_to_bookings.rb
│ ├── 20150324235432_add_in_progress_to_bookings.rb
│ ├── 20150325155804_change_car_booked_time_column.rb
│ ├── 20150323213950_add_completed_fields_to_booking.rb
│ ├── 20150322194743_create_users.rb
│ └── 20150322234851_create_bookings.rb
├── seeds.rb
└── schema.rb
├── clock.rb
├── Rakefile
├── .gitignore
├── Gemfile
└── Gemfile.lock
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Schedule and book cars for car2go in Vancouver
2 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/views/bookings/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= react_component 'NewBookingPage' %>
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
2 | clock: bundle exec clockwork clock.rb
3 |
--------------------------------------------------------------------------------
/app/assets/images/auth-flow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brentvatne/concierge/HEAD/app/assets/images/auth-flow.gif
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/_variables.scss:
--------------------------------------------------------------------------------
1 | $spinkit-spinner-margin: 0 auto !default;
2 | $spinkit-spinner-color: #333 !default;
3 |
--------------------------------------------------------------------------------
/app/views/bookings/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= react_component 'EditBookingPage',
2 | {booking: BookingSerializer.new(@booking, root: false) } %>
3 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/config/initializers/twilio.rb:
--------------------------------------------------------------------------------
1 | Twilio.configure do |config|
2 | config.account_sid = ENV['TWILIO_SID']
3 | config.auth_token = ENV['TWILIO_AUTH_TOKEN']
4 | end
5 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/config/initializers/geocoder.rb:
--------------------------------------------------------------------------------
1 | Geokit::default_units = :meters
2 | Geokit::default_formula = :sphere
3 | Geokit::Geocoders::GoogleGeocoder.api_key = ENV['GOOGLE_API_KEY']
4 |
--------------------------------------------------------------------------------
/lib/tasks/perform_bookings.rake:
--------------------------------------------------------------------------------
1 | task :perform_bookings => [:environment] do
2 | PerformBookings.execute(
3 | Booking.within_booking_window.incomplete
4 | )
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components.js:
--------------------------------------------------------------------------------
1 | //= require_self
2 | //= require ./components/has_drop_menu
3 | //= require_tree ./components
4 |
5 | var self, window, global = global || window || self;
6 |
--------------------------------------------------------------------------------
/config/initializers/active_model_serializer.rb:
--------------------------------------------------------------------------------
1 | # Convert attributes from snake_case to lowerCamelCase
2 | ActiveModel::Serializer.setup do |config|
3 | config.key_format = :lower_camel
4 | end
5 |
--------------------------------------------------------------------------------
/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/20150322223317_add_password_digest_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddPasswordDigestToUsers < ActiveRecord::Migration
2 | def change
3 | add_column :users, :password_digest, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150324011147_add_car_plate_to_bookings.rb:
--------------------------------------------------------------------------------
1 | class AddCarPlateToBookings < ActiveRecord::Migration
2 | def change
3 | add_column :bookings, :car_license_plate, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150324235103_add_asap_to_bookings.rb:
--------------------------------------------------------------------------------
1 | class AddAsapToBookings < ActiveRecord::Migration
2 | def change
3 | add_column :bookings, :asap, :boolean, default: false, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150325034141_add_reservation_id_to_bookings.rb:
--------------------------------------------------------------------------------
1 | class AddReservationIdToBookings < ActiveRecord::Migration
2 | def change
3 | add_column :bookings, :reservation_id, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150325034449_add_reservation_response_to_bookings.rb:
--------------------------------------------------------------------------------
1 | class AddReservationResponseToBookings < ActiveRecord::Migration
2 | def change
3 | add_column :bookings, :reservation_response, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150324235432_add_in_progress_to_bookings.rb:
--------------------------------------------------------------------------------
1 | class AddInProgressToBookings < ActiveRecord::Migration
2 | def change
3 | add_column :bookings, :in_progress, :boolean, default: false, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/app/views/pages/home.html.erb:
--------------------------------------------------------------------------------
1 | <% if current_user.present? %>
2 | <%= react_component 'ListPage', {upcomingBookings: @upcoming_bookings} %>
3 | <% else %>
4 | <%= react_component 'LoginPage', {authorizationUrl: @authorization_url} %>
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/db/migrate/20150325155804_change_car_booked_time_column.rb:
--------------------------------------------------------------------------------
1 | class ChangeCarBookedTimeColumn < ActiveRecord::Migration
2 | def change
3 | remove_column :bookings, :car_booked_time
4 | add_column :bookings, :car_booked_time, :datetime
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/clock.rb:
--------------------------------------------------------------------------------
1 | require 'clockwork'
2 | require_relative './config/boot'
3 | require_relative './config/environment'
4 |
5 | include Clockwork
6 |
7 | handler do |job|
8 | end
9 |
10 | every(1.minute, 'Performing bookings') {
11 | `rake perform_bookings`
12 | }
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Rails.application.config.session_store :cookie_store, key: '_concierge_session'
4 | Rails.application.config.session_store ActionDispatch::Session::CacheStore, expire_after: 3.months
5 |
--------------------------------------------------------------------------------
/db/migrate/20150323213950_add_completed_fields_to_booking.rb:
--------------------------------------------------------------------------------
1 | class AddCompletedFieldsToBooking < ActiveRecord::Migration
2 | def change
3 | add_column :bookings, :car_address, :string
4 | add_column :bookings, :car_distance, :string
5 | add_column :bookings, :car_booked_time, :string
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20150322194743_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table :users do |t|
4 | t.string :oauth_token_secret
5 | t.string :oauth_token
6 | t.string :email
7 | t.string :phone
8 | t.string :name
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 |
3 | def home
4 | if current_user.present?
5 | @upcoming_bookings = current_user.bookings.upcoming_or_active.order('time ASC')
6 | @upcoming_bookings.to_a.map! { |b| BookingSerializer.new(b, root: false) }
7 | end
8 | end
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/api_client.js:
--------------------------------------------------------------------------------
1 | global.ApiClient = {
2 | bookNow: function(data) {
3 | $.post('/book-now', data).
4 | complete(function() { window.location.reload() })
5 | },
6 |
7 | addressForCoords: function(data, callback) {
8 | $.get('/locations/address-for-coords', data).
9 | success(callback);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 |
3 | def create
4 | @user = User.find_by_email(params[:email])
5 |
6 | if @user.authenticate(params[:password])
7 | session[:current_user_id] = @user.id
8 | render json: {success: true}
9 | else
10 | render json: {success: false}
11 | end
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinkit.scss:
--------------------------------------------------------------------------------
1 | @import
2 | "spinners/1-rotating-plane",
3 | "spinners/2-double-bounce",
4 | "spinners/3-wave",
5 | "spinners/4-wandering-cubes",
6 | "spinners/5-pulse",
7 | "spinners/6-chasing-dots",
8 | "spinners/7-three-bounce",
9 | "spinners/8-circle",
10 | "spinners/9-cube-grid",
11 | "spinners/9-wordpress",
12 | "spinners/10-fading-circle";
13 |
--------------------------------------------------------------------------------
/db/migrate/20150322234851_create_bookings.rb:
--------------------------------------------------------------------------------
1 | class CreateBookings < ActiveRecord::Migration
2 | def change
3 | create_table :bookings do |t|
4 | t.string :title
5 | t.datetime :time
6 | t.string :address
7 | t.float :lat
8 | t.float :lon
9 | t.boolean :complete, default: false, null: false
10 | t.references :user
11 | t.timestamps
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/views/pages/rentals.html.erb:
--------------------------------------------------------------------------------
1 |
Rentals:
2 |
3 |
4 | Cost
5 | Start
6 | End
7 |
8 | <% @rentals.each do |rental| %>
9 |
10 | <%= rental.cost %>
11 | <%= rental.start_location %> at <%= rental.start_time %>
12 | <%= rental.end_location %> at <%= rental.end_time %>
13 |
14 | <% end %>
15 |
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/has_timer.js.jsx:
--------------------------------------------------------------------------------
1 | global.HasTimer = {
2 | componentWillMount: function() {
3 | this.intervals = [];
4 | },
5 |
6 | setInterval: function() {
7 | this.intervals.push(setInterval.apply(null, arguments));
8 | },
9 |
10 | componentWillUnmount: function() {
11 | this.clearIntervals();
12 | },
13 |
14 | clearIntervals: function() {
15 | this.intervals.map(clearInterval);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/serializers/booking_serializer.rb:
--------------------------------------------------------------------------------
1 | class BookingSerializer < ActiveModel::Serializer
2 | attributes :id, :title, :time, :day, :date, :address, :lat, :lon, :complete,
3 | :car_address, :car_license_plate, :in_progress
4 |
5 | def time
6 | object.time.strftime('%l:%M %p')
7 | end
8 |
9 | def day
10 | object.time.strftime('%A')
11 | end
12 |
13 | def date
14 | object.time.strftime('%B %d, %Y')
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/controllers/locations_controller.rb:
--------------------------------------------------------------------------------
1 | class LocationsController < ApplicationController
2 |
3 | def address_for_coords
4 | result = Geokit::Geocoders::GoogleGeocoder.geocode("#{params[:lat]} #{params[:lon]}")
5 |
6 | if result.success?
7 | render json: {
8 | address: result.full_address
9 | }
10 | else
11 | render json: {
12 | error: "Could not find address"
13 | }
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore all logfiles and tempfiles.
11 | /log/*
12 | !/log/.keep
13 | /tmp
14 | .env
15 | lib/car2go/**/*
16 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/5-pulse.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | */
7 | @import "../variables";
8 |
9 | .sk-spinner-pulse {
10 | &.sk-spinner {
11 | width: 40px;
12 | height: 40px;
13 | margin: $spinkit-spinner-margin;
14 | background-color: $spinkit-spinner-color;
15 |
16 | border-radius: 100%;
17 | animation: sk-pulseScaleOut 1.0s infinite ease-in-out;
18 | }
19 | }
20 |
21 | @keyframes sk-pulseScaleOut {
22 | 0% {
23 | transform: scale(0.0);
24 | } 100% {
25 | transform: scale(1.0);
26 | opacity: 0;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 |
3 | def create
4 | @user = User.create(
5 | oauth_token_secret: session[:oauth_token_secret],
6 | oauth_token: session[:oauth_token],
7 | name: user_params[:name],
8 | email: user_params[:email],
9 | phone: user_params[:phone],
10 | password: user_params[:password],
11 | password_confirmation: user_params[:password]
12 | )
13 |
14 | session[:current_user_id] = @user.id
15 | render json: {success: true, id: @user.id}
16 | end
17 |
18 | def user_params
19 | params.require(:user).permit(:name, :email, :phone, :password)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/config/unicorn.rb:
--------------------------------------------------------------------------------
1 | worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
2 | timeout 15
3 | preload_app true
4 |
5 | before_fork do |server, worker|
6 | Signal.trap 'TERM' do
7 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
8 | Process.kill 'QUIT', Process.pid
9 | end
10 |
11 | defined?(ActiveRecord::Base) and
12 | ActiveRecord::Base.connection.disconnect!
13 | end
14 |
15 | after_fork do |server, worker|
16 | Signal.trap 'TERM' do
17 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
18 | end
19 |
20 | defined?(ActiveRecord::Base) and
21 | ActiveRecord::Base.establish_connection
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/authentication_controller.rb:
--------------------------------------------------------------------------------
1 | class AuthenticationController < ApplicationController
2 |
3 | def new
4 | @authorization_url = ApiClient.authorization_url(session)
5 | end
6 |
7 | def create
8 | client = ApiClient.new(session[:oauth_token_secret], session[:oauth_token])
9 | tokens = client.get_access_tokens(params[:verification_code])
10 |
11 | if tokens[:oauth_token].present? && tokens[:oauth_token_secret].present?
12 | session[:oauth_token] = tokens[:oauth_token]
13 | session[:oauth_token_secret] = tokens[:oauth_token_secret]
14 | render json: {success: true}
15 | else
16 | render json: {success: false}
17 | end
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'rails', '4.2.0'
4 | gem 'pg'
5 | gem 'sass-rails', '~> 5.0'
6 | gem 'uglifier', '>= 1.3.0'
7 | gem 'coffee-rails', '~> 4.1.0'
8 | gem 'jquery-rails'
9 | gem 'jbuilder', '~> 2.0'
10 | gem 'react-rails', '~> 1.0.0.pre', github: 'reactjs/react-rails'
11 | gem 'httparty'
12 | gem 'autoprefixer-rails'
13 | gem 'bcrypt', '~> 3.1.7'
14 | gem 'geokit'
15 | gem 'active_model_serializers'
16 | gem 'chronic'
17 | gem 'twilio-ruby'
18 | gem 'clockwork'
19 | gem 'dalli'
20 |
21 | group :production do
22 | gem 'unicorn'
23 | gem 'rails_12factor'
24 | end
25 |
26 | group :development, :test do
27 | gem 'dotenv-rails'
28 | gem 'byebug'
29 | gem 'web-console', '~> 2.0'
30 | end
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/has_drop_menu.js.jsx:
--------------------------------------------------------------------------------
1 | global.HasDropMenu = {
2 | getInitialState: function() {
3 | return {
4 | menuIsOpen: false
5 | }
6 | },
7 |
8 | componentDidMount: function() {
9 | $('body').on('click', this.clickOuter);
10 | },
11 |
12 | componentWillUnmount: function() {
13 | $('body').off('click', this.clickOuter);
14 | },
15 |
16 | clickOuter: function(e) {
17 | var root = this.refs.root.getDOMNode();
18 | if (! $.contains(root, e.target) && root != e.target) {
19 | this.setState({menuIsOpen: false});
20 | }
21 | },
22 |
23 | toggleMenu: function(e) {
24 | e.preventDefault();
25 | this.setState({menuIsOpen: !this.state.menuIsOpen})
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/views/authentication/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
This should have opened in a new window. Car2Go doesn't yet have a slick
5 | way of integrating with 3rd party apps, so once you have approved access
6 | we'll need you to copy the verification code then close this popup and
7 | paste the code into the verfication code field back on our site .
8 | Allow me to demonstrate:
9 |
10 | <%= image_tag 'auth-flow.gif', class: 'auth-flow-example' %>
11 |
12 |
13 | Got it, let's do this
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/loading_overlay.js.jsx:
--------------------------------------------------------------------------------
1 | global.LoadingOverlay = React.createClass({
2 | getDefaultProps: function() {
3 | return {
4 | isVisible: false
5 | }
6 | },
7 |
8 | render: function() {
9 | var classes = React.addons.classSet({
10 | 'loading-overlay': true,
11 | 'loading-overlay--is-visible': this.props.isVisible
12 | });
13 |
14 | return (
15 |
23 | )
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/1-rotating-plane.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | */
7 | @import "../variables";
8 |
9 | .sk-spinner-rotating-plane {
10 | &.sk-spinner {
11 | width: 30px;
12 | height: 30px;
13 | background-color: $spinkit-spinner-color;
14 |
15 | margin: $spinkit-spinner-margin;
16 | animation: sk-rotatePlane 1.2s infinite ease-in-out;
17 | }
18 | }
19 |
20 | @keyframes sk-rotatePlane {
21 | 0% {
22 | transform: perspective(120px) rotateX(0deg) rotateY(0deg);
23 | } 50% {
24 | transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
25 | } 100% {
26 | transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/9-wordpress.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | *
7 | *
8 | */
9 | @import "../variables";
10 |
11 | .sk-spinner-wordpress {
12 | &.sk-spinner {
13 | background-color: $spinkit-spinner-color;
14 | width: 30px;
15 | height: 30px;
16 | border-radius: 30px;
17 | position: relative;
18 | margin: $spinkit-spinner-margin;
19 | animation: sk-innerCircle 1s linear infinite;
20 | }
21 |
22 | .sk-inner-circle {
23 | display: block;
24 | background-color: #fff;
25 | width: 8px;
26 | height: 8px;
27 | position: absolute;
28 | border-radius: 8px;
29 | top: 5px;
30 | left: 5px;
31 | }
32 | }
33 |
34 | @keyframes sk-innerCircle {
35 | 0% { transform: rotate(0) }
36 | 100% { transform: rotate(360deg) }
37 | }
38 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | # path to your application root.
5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6 |
7 | Dir.chdir APP_ROOT do
8 | # This script is a starting point to setup your application.
9 | # Add necessary setup steps to this file:
10 |
11 | puts "== Installing dependencies =="
12 | system "gem install bundler --conservative"
13 | system "bundle check || bundle install"
14 |
15 | # puts "\n== Copying sample files =="
16 | # unless File.exist?("config/database.yml")
17 | # system "cp config/database.yml.sample config/database.yml"
18 | # end
19 |
20 | puts "\n== Preparing database =="
21 | system "bin/rake db:setup"
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system "rm -f log/*"
25 | system "rm -rf tmp/cache"
26 |
27 | puts "\n== Restarting application server =="
28 | system "touch tmp/restart.txt"
29 | end
30 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/2-double-bounce.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
8 | *
9 | */
10 | @import "../variables";
11 |
12 | .sk-spinner-double-bounce {
13 | &.sk-spinner {
14 | width: 40px;
15 | height: 40px;
16 |
17 | position: relative;
18 | margin: $spinkit-spinner-margin;
19 | }
20 |
21 | .sk-double-bounce1, .sk-double-bounce2 {
22 | width: 100%;
23 | height: 100%;
24 | border-radius: 50%;
25 | background-color: $spinkit-spinner-color;
26 | opacity: 0.6;
27 | position: absolute;
28 | top: 0;
29 | left: 0;
30 |
31 | animation: sk-doubleBounce 2.0s infinite ease-in-out;
32 | }
33 |
34 | .sk-double-bounce2 {
35 | animation-delay: -1.0s;
36 | }
37 | }
38 |
39 | @keyframes sk-doubleBounce {
40 | 0%, 100% { transform: scale(0.0) }
41 | 50% { transform: scale(1.0) }
42 | }
43 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 44ae6ffcc518d32a8586bb5addcaa9ddb0eeca72ac1bf17532b60a0afcbe6a05103c30f366422ae6fd0496317a573a9afc4a27687caaa5254786ebd682b09c6b
15 |
16 | test:
17 | secret_key_base: 5d6bc4e25f1476fdbcc30e6c9115d55822bf7a87904d7f08ac9b6c64ce4e67907b393da56f1812edcf49b27b08683a03e8b2a10ea11a495aac32eb5011b1f906
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/book_now_button.js.jsx:
--------------------------------------------------------------------------------
1 | global.BookNowButton = React.createClass({
2 |
3 | getInitialState: function() {
4 | return {
5 | isLoading: false
6 | }
7 | },
8 |
9 | createBooking: function(e) {
10 | e.preventDefault();
11 | var self = this;
12 |
13 | self.setState({isLoading: true});
14 |
15 | var bookNow = function(position) {
16 | var data = {lat: position.coords.latitude, lon: position.coords.longitude}
17 | ApiClient.bookNow(data);
18 | };
19 |
20 | var failedToFindLocation = function() {
21 | alert("Sorry, your location could not be found");
22 | };
23 |
24 | navigator.geolocation.getCurrentPosition(bookNow, failedToFindLocation, {
25 | maximumAge: 60000, enableHighAccuracy: true
26 | });
27 | },
28 |
29 | render: function() {
30 | return (
31 |
32 |
33 |
34 | Book now
35 |
36 |
37 | )
38 | }
39 |
40 | });
41 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/3-wave.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | *
7 | *
8 | *
9 | *
10 | *
11 | *
12 | */
13 | @import "../variables";
14 |
15 | .sk-spinner-wave {
16 | &.sk-spinner {
17 | margin: $spinkit-spinner-margin;
18 | width: 50px;
19 | height: 30px;
20 | text-align: center;
21 | font-size: 10px;
22 | }
23 |
24 | div {
25 | background-color: $spinkit-spinner-color;
26 | height: 100%;
27 | width: 6px;
28 | display: inline-block;
29 |
30 | animation: sk-waveStretchDelay 1.2s infinite ease-in-out;
31 | }
32 |
33 | .sk-rect2 { animation-delay: -1.1s }
34 | .sk-rect3 { animation-delay: -1.0s }
35 | .sk-rect4 { animation-delay: -0.9s }
36 | .sk-rect5 { animation-delay: -0.8s }
37 | }
38 |
39 | @keyframes sk-waveStretchDelay {
40 | 0%, 40%, 100% { transform: scaleY(0.4) }
41 | 20% { transform: scaleY(1.0) }
42 | }
43 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/7-three-bounce.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
9 | *
10 | */
11 | @import "../variables";
12 |
13 | .sk-spinner-three-bounce {
14 | &.sk-spinner {
15 | margin: $spinkit-spinner-margin;
16 | width: 70px;
17 | text-align: center;
18 | }
19 |
20 | div {
21 | width: 18px;
22 | height: 18px;
23 | background-color: $spinkit-spinner-color;
24 |
25 | border-radius: 100%;
26 | display: inline-block;
27 | animation: sk-threeBounceDelay 1.4s infinite ease-in-out;
28 | /* Prevent first frame from flickering when animation starts */
29 | animation-fill-mode: both;
30 | }
31 |
32 | .sk-bounce1 {
33 | animation-delay: -0.32s;
34 | }
35 |
36 | .sk-bounce2 {
37 | animation-delay: -0.16s;
38 | }
39 | }
40 |
41 | @keyframes sk-threeBounceDelay {
42 | 0%, 80%, 100% {
43 | transform: scale(0.0);
44 | } 40% {
45 | transform: scale(1.0);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 |
6 | # Disable the root node, eg: {projects: [{..}, {..}]}
7 | def default_serializer_options
8 | {root: false}
9 | end
10 |
11 | def current_user
12 | return nil if session[:current_user_id].nil?
13 | @current_user ||= begin
14 | user = User.find_by_id(session[:current_user_id])
15 |
16 | # Clear it if no user was found with that id
17 | if user.nil?
18 | session[:current_user_id] = nil
19 | end
20 |
21 | user
22 | end
23 | end
24 | helper_method :current_user
25 |
26 | before_filter :deep_snake_case_params!
27 | def deep_snake_case_params!(val = params)
28 | case val
29 | when Array
30 | val.map {|v| deep_snake_case_params! v }
31 | when Hash
32 | val.keys.each do |k, v = val[k]|
33 | val.delete k
34 | val[k.underscore] = deep_snake_case_params!(v)
35 | end
36 | val
37 | else
38 | val
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/new_booking_page.js.jsx:
--------------------------------------------------------------------------------
1 | global.NewBookingPage = React.createClass({
2 |
3 | getInitialState: function() {
4 | return {
5 | isLoading: false
6 | }
7 | },
8 |
9 | submitForm: function(booking) {
10 | var self = this,
11 | data = {booking: booking};
12 |
13 | this.setState({isLoading: true});
14 |
15 | $.post('/bookings', data).done(function(response) {
16 | if (response.success == true) {
17 | window.location.href = '/'
18 | } else {
19 | self.setState({isLoading: false});
20 | alert('Something went wrong')
21 | }
22 | }).complete(function() {
23 | // Problem!
24 | });
25 | },
26 |
27 | render: function() {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
Schedule a new booking
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/primary_navigation.js.jsx:
--------------------------------------------------------------------------------
1 | global.PrimaryNavigation = React.createClass({
2 | getDefaultProps: function() {
3 | return {
4 | extraClasses: '',
5 | showScheduleBooking: false,
6 | showBackHome: false
7 | }
8 | },
9 |
10 | render: function() {
11 | var links = [];
12 |
13 | if (this.props.showBookNow) {
14 | links.push( )
15 | }
16 |
17 | if (this.props.showScheduleBooking) {
18 | links.push(
19 |
20 | Schedule for later
21 |
22 | )
23 | }
24 |
25 | if (this.props.showBackHome) {
26 | links.push(
27 |
28 | Go back home
29 |
30 | )
31 | }
32 |
33 | return (
34 |
35 |
36 |
Concierge
37 |
38 |
39 | {links}
40 |
41 |
42 |
43 | )
44 | }
45 | });
46 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/6-chasing-dots.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
8 | *
9 | */
10 | @import "../variables";
11 |
12 | .sk-spinner-chasing-dots {
13 | &.sk-spinner {
14 | margin: $spinkit-spinner-margin;
15 | width: 40px;
16 | height: 40px;
17 | position: relative;
18 | text-align: center;
19 |
20 | animation: sk-chasingDotsRotate 2.0s infinite linear;
21 | }
22 |
23 | .sk-dot1, .sk-dot2 {
24 | width: 60%;
25 | height: 60%;
26 | display: inline-block;
27 | position: absolute;
28 | top: 0;
29 | background-color: $spinkit-spinner-color;
30 | border-radius: 100%;
31 |
32 | animation: sk-chasingDotsBounce 2.0s infinite ease-in-out;
33 | }
34 |
35 | .sk-dot2 {
36 | top: auto;
37 | bottom: 0px;
38 | animation-delay: -1.0s;
39 | }
40 | }
41 |
42 | @keyframes sk-chasingDotsRotate {
43 | 100% {
44 | transform: rotate(360deg);
45 | }
46 | }
47 |
48 | @keyframes sk-chasingDotsBounce {
49 | 0%, 100% {
50 | transform: scale(0.0);
51 | } 50% {
52 | transform: scale(1.0);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require_self
16 | //= require react
17 | //= require react_ujs
18 | //= require components
19 | //= require_tree .
20 |
21 | $.ajaxSetup({
22 | headers: {
23 | 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
24 | }
25 | });
26 |
27 | $.put = function(url, data, callback, type){
28 | if ( $.isFunction(data) ){
29 | type = type || callback,
30 | callback = data,
31 | data = {}
32 | }
33 |
34 | return $.ajax({
35 | url: url,
36 | type: 'PUT',
37 | success: callback,
38 | data: data,
39 | contentType: type
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/4-wandering-cubes.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
8 | *
9 | */
10 | @import "../variables";
11 |
12 | .sk-spinner-wandering-cubes {
13 | &.sk-spinner {
14 | margin: $spinkit-spinner-margin;
15 | width: 32px;
16 | height: 32px;
17 | position: relative;
18 | }
19 |
20 | .sk-cube1, .sk-cube2 {
21 | background-color: $spinkit-spinner-color;
22 | width: 10px;
23 | height: 10px;
24 | position: absolute;
25 | top: 0;
26 | left: 0;
27 |
28 | animation: sk-wanderingCubeMove 1.8s infinite ease-in-out;
29 | }
30 |
31 | .sk-cube2 {
32 | animation-delay: -0.9s;
33 | }
34 | }
35 |
36 | @keyframes sk-wanderingCubeMove {
37 | 25% {
38 | transform: translateX(42px) rotate(-90deg) scale(0.5);
39 | } 50% {
40 | /* Hack to make FF rotate in the right direction */
41 | transform: translateX(42px) translateY(42px) rotate(-179deg);
42 | } 50.1% {
43 | transform: translateX(42px) translateY(42px) rotate(-180deg);
44 | } 75% {
45 | transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5);
46 | } 100% {
47 | transform: rotate(-360deg);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/edit_booking_page.js.jsx:
--------------------------------------------------------------------------------
1 | global.EditBookingPage = React.createClass({
2 | getDefaultProps: function() {
3 | return { booking: {} }
4 | },
5 |
6 | getInitialState: function() {
7 | return {
8 | isLoading: false
9 | }
10 | },
11 |
12 | submitForm: function(booking) {
13 | var self = this,
14 | data = {booking: booking};
15 |
16 | this.setState({isLoading: true});
17 |
18 | $.put('/bookings/' + this.props.booking.id, data).done(function(response) {
19 | if (response.success == true) {
20 | window.location.href = '/'
21 | } else {
22 | self.setState({isLoading: false});
23 | alert('Something went wrong')
24 | }
25 | }).complete(function() {
26 | // Problem!
27 | });
28 | },
29 |
30 | render: function() {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
Edit booking
38 |
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Concierge
5 |
6 |
7 | <%= csrf_meta_tags %>
8 | <%= stylesheet_link_tag 'application', media: 'all' %>
9 |
10 | <%= javascript_include_tag 'application' %>
11 |
21 |
22 |
23 | <%= yield %>
24 |
25 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/booking_pending_button.js.jsx:
--------------------------------------------------------------------------------
1 | global.BookingPendingButton = React.createClass({
2 | mixins: [HasDropMenu],
3 |
4 | getInitialState: function() {
5 | return {
6 | isLoading: false
7 | }
8 | },
9 |
10 | deleteBooking: function(e) {
11 | this.setState({isLoading: true});
12 | this.props.onDelete(e);
13 | },
14 |
15 | editBooking: function(e) {
16 | e.preventDefault()
17 | this.setState({isLoading: true});
18 | window.location.href = "/bookings/" + this.props.bookingId + "/edit"
19 | },
20 |
21 | render: function() {
22 | var self = this,
23 | cx = React.addons.classSet,
24 | editLink,
25 | optionsListClasses = cx({'link-options-list': true,
26 | 'active': this.state.menuIsOpen});
27 |
28 | if (this.props.stage == "upcoming") {
29 | editLink = (
30 | Edit this
31 | )
32 | }
33 |
34 | return (
35 |
50 | )
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/9-cube-grid.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | *
7 | *
8 | *
9 | *
10 | *
11 | *
12 | *
13 | *
14 | *
15 | *
16 | */
17 | @import "../variables";
18 |
19 | .sk-spinner-cube-grid {
20 | &.sk-spinner {
21 | width: 30px;
22 | height: 30px;
23 | margin: $spinkit-spinner-margin;
24 | }
25 |
26 | .sk-cube {
27 | width: 33%;
28 | height: 33%;
29 | background-color: $spinkit-spinner-color;
30 | float: left;
31 | animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
32 | }
33 |
34 | /*
35 | * Spinner positions
36 | * 1 2 3
37 | * 4 5 6
38 | * 7 8 9
39 | */
40 |
41 | .sk-cube:nth-child(1) { animation-delay: 0.2s }
42 | .sk-cube:nth-child(2) { animation-delay: 0.3s }
43 | .sk-cube:nth-child(3) { animation-delay: 0.4s }
44 | .sk-cube:nth-child(4) { animation-delay: 0.1s }
45 | .sk-cube:nth-child(5) { animation-delay: 0.2s }
46 | .sk-cube:nth-child(6) { animation-delay: 0.3s }
47 | .sk-cube:nth-child(7) { animation-delay: 0.0s }
48 | .sk-cube:nth-child(8) { animation-delay: 0.1s }
49 | .sk-cube:nth-child(9) { animation-delay: 0.2s }
50 | }
51 |
52 | @keyframes sk-cubeGridScaleDelay {
53 | 0%, 70%, 100% { transform:scale3D(1.0, 1.0, 1.0) }
54 | 35% { transform:scale3D(0.0, 0.0, 1.0) }
55 | }
56 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/booking_complete_button.js.jsx:
--------------------------------------------------------------------------------
1 | global.BookingCompleteButton = React.createClass({
2 | mixins: [HasDropMenu],
3 |
4 | getInitialState: function() {
5 | return {
6 | isLoading: false
7 | }
8 | },
9 |
10 | cancelReservation: function() {
11 | this.setState({isLoading: true});
12 |
13 | $.post('/bookings/' + this.props.bookingId + '/cancel', {}).complete(function() {
14 | window.location.reload();
15 | })
16 | },
17 |
18 | deleteBooking: function(e) {
19 | this.setState({isLoading: true});
20 | this.props.onDelete(e);
21 | },
22 |
23 | render: function() {
24 | var self = this,
25 | cx = React.addons.classSet,
26 | action,
27 | optionsListClasses = cx({'link-options-list': true,
28 | 'active': this.state.menuIsOpen});
29 |
30 | if (this.props.stage == 'upcoming') {
31 | action = (
32 | Cancel reservation
33 | )
34 | } else {
35 | action = (
36 | Done, delete this
37 | )
38 | }
39 |
40 | return (
41 |
55 | )
56 | }
57 | });
58 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
31 | # yet still be able to expire them through the digest params.
32 | config.assets.digest = true
33 |
34 | # Adds additional error checking when serving assets at runtime.
35 | # Checks for improperly declared sprockets dependencies.
36 | # Raises helpful error messages.
37 | config.assets.raise_runtime_errors = true
38 |
39 | # Raises error for missing translations
40 | # config.action_view.raise_on_missing_translations = true
41 | end
42 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | has_secure_password
3 | has_many :bookings
4 | validates :email, uniqueness: true
5 |
6 | def rentals
7 | @rentals ||= api_client.rentals.map { |rental|
8 | Rental.new(rental)
9 | }
10 | end
11 |
12 | def accounts
13 | @accounts ||= api_client.accounts
14 | end
15 |
16 | def account_id
17 | @account_id ||= accounts.first['accountId']
18 | end
19 |
20 | def create_booking(vin)
21 | if response = api_client.create_booking(vin, account_id)
22 | response
23 | else
24 | nil
25 | end
26 | end
27 |
28 | def cancel_booking(id)
29 | api_client.cancel_booking(id)
30 | end
31 |
32 | private
33 |
34 | def api_client
35 | @api_client ||= ApiClient.new(oauth_token_secret, oauth_token)
36 | end
37 |
38 | class Rental
39 | def initialize(raw_data)
40 | @raw_data = raw_data
41 | end
42 |
43 | def to_s
44 | "#{cost} - #{start_time}"
45 | end
46 |
47 | def inspect
48 | to_s
49 | end
50 |
51 | def cost
52 | "$#{@raw_data['driveAmount']['amountGross']} #{@raw_data['driveAmount']['currency']}"
53 | end
54 |
55 | def duration
56 | @raw_data['driveDurationInMinutes']
57 | end
58 |
59 | def start_location
60 | @raw_data['usageStartAddress']
61 | end
62 |
63 | def end_location
64 | @raw_data['usageEndAddress']
65 | end
66 |
67 | def start_time
68 | time = @raw_data['usageStartTime']['timeInMillis']
69 | if time.present?
70 | Time.at(time / 1000)
71 | end
72 | end
73 |
74 | def end_time
75 | time = @raw_data['usageEndTime']['timeInMillis']
76 | if time.present?
77 | Time.at(time / 1000)
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | # Pick the frameworks you want:
4 | require "active_model/railtie"
5 | require "active_job/railtie"
6 | require "active_record/railtie"
7 | require "action_controller/railtie"
8 | require "action_mailer/railtie"
9 | require "action_view/railtie"
10 | require "sprockets/railtie"
11 | # require "rails/test_unit/railtie"
12 |
13 | # Require the gems listed in Gemfile, including any gems
14 | # you've limited to :test, :development, or :production.
15 | Bundler.require(*Rails.groups)
16 |
17 | module Concierge
18 | class Application < Rails::Application
19 | # Settings in config/environments/* take precedence over those specified here.
20 | # Application configuration should go into files in config/initializers
21 | # -- all .rb files in that directory are automatically loaded.
22 |
23 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
24 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
25 | # config.time_zone = 'Central Time (US & Canada)'
26 | config.time_zone = 'Pacific Time (US & Canada)'
27 | Time.zone = 'Pacific Time (US & Canada)'
28 | Chronic.time_class = Time.zone
29 |
30 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
31 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
32 | # config.i18n.default_locale = :de
33 | config.autoload_paths += %W(#{config.root}/app/serializers)
34 | config.autoload_paths += %W(#{config.root}/app/services)
35 | config.react.addons = true
36 |
37 | # Do not swallow errors in after_commit/after_rollback callbacks.
38 | config.active_record.raise_in_transactional_callbacks = true
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/services/perform_bookings.rb:
--------------------------------------------------------------------------------
1 | class PerformBookings
2 |
3 | def self.execute(bookings)
4 | new(bookings).execute
5 | end
6 |
7 | def initialize(bookings)
8 | @bookings = Array.wrap(bookings)
9 | end
10 |
11 | def execute
12 | @bookings.each do |booking|
13 | booking.update_attributes(in_progress: true) unless booking.in_progress?
14 |
15 | available_cars_for(booking).each do |car|
16 | begin
17 | puts "attempting to book #{car['vin']}"
18 | break if booking.perform!(car['vin'])
19 | rescue Exception => e
20 | Rails.logger.info(e.backtrace)
21 | end
22 | end
23 |
24 | if booking.complete?
25 | after_complete(booking)
26 | else
27 | puts "Nothing close enough to #{booking.id}"
28 | end
29 | end
30 | end
31 |
32 | private
33 |
34 | def after_complete(booking)
35 | begin
36 | twilio.messages.create(
37 | from: '+16042278434',
38 | to: booking.user.phone,
39 | body: "Car booked: #{booking.car_license_plate} at #{booking.car_address}"
40 | )
41 | rescue Exception => e
42 | puts "Error while sending text message:"
43 | Rails.logger.info(e.backtrace)
44 | end
45 | end
46 |
47 | def available_cars_for(booking)
48 | available_cars.
49 | map { |car| car['distance'] = booking.distance_to(car['location']); car }.
50 | find_all { |car| car['distance'] <= 500 }.
51 | sort_by { |car| car['distance'] }
52 | end
53 |
54 | def available_cars
55 | @available_cars ||= ApiClient.available_cars.map { |car|
56 | lng = car['coordinates'].first
57 | lat = car['coordinates'].second
58 | car['location'] = Geokit::GeoLoc.new(lat: lat, lng: lng)
59 | car
60 | }
61 | end
62 |
63 | def twilio
64 | @twilio ||= Twilio::REST::Client.new
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Randomize the order test cases are executed.
35 | config.active_support.test_order = :random
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20150325155804) do
15 |
16 | # These are extensions that must be enabled in order to support this database
17 | enable_extension "plpgsql"
18 |
19 | create_table "bookings", force: :cascade do |t|
20 | t.string "title"
21 | t.datetime "time"
22 | t.string "address"
23 | t.float "lat"
24 | t.float "lon"
25 | t.boolean "complete", default: false, null: false
26 | t.integer "user_id"
27 | t.datetime "created_at"
28 | t.datetime "updated_at"
29 | t.string "car_address"
30 | t.string "car_distance"
31 | t.string "car_license_plate"
32 | t.boolean "asap", default: false, null: false
33 | t.boolean "in_progress", default: false, null: false
34 | t.string "reservation_id"
35 | t.text "reservation_response"
36 | t.datetime "car_booked_time"
37 | end
38 |
39 | create_table "users", force: :cascade do |t|
40 | t.string "oauth_token_secret"
41 | t.string "oauth_token"
42 | t.string "email"
43 | t.string "phone"
44 | t.string "name"
45 | t.datetime "created_at"
46 | t.datetime "updated_at"
47 | t.string "password_digest"
48 | end
49 |
50 | end
51 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root 'pages#home'
3 | get '/auth/new' => 'authentication#new'
4 | post '/auth' => 'authentication#create'
5 | resources :users
6 | resources :sessions
7 |
8 | get '/new-booking' => 'bookings#new'
9 | post '/book-now' => 'bookings#now'
10 | resources :bookings do
11 | member do
12 | post :cancel
13 | end
14 | collection do
15 | get :upcoming
16 | get :past
17 | end
18 | end
19 |
20 | get '/locations/address-for-coords' => 'locations#address_for_coords'
21 |
22 |
23 | # Example of regular route:
24 | # get 'products/:id' => 'catalog#view'
25 |
26 | # Example of named route that can be invoked with purchase_url(id: product.id)
27 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
28 |
29 | # Example resource route (maps HTTP verbs to controller actions automatically):
30 | # resources :products
31 |
32 | # Example resource route with options:
33 | # resources :products do
34 | # member do
35 | # get 'short'
36 | # post 'toggle'
37 | # end
38 | #
39 | # collection do
40 | # get 'sold'
41 | # end
42 | # end
43 |
44 | # Example resource route with sub-resources:
45 | # resources :products do
46 | # resources :comments, :sales
47 | # resource :seller
48 | # end
49 |
50 | # Example resource route with more complex sub-resources:
51 | # resources :products do
52 | # resources :comments
53 | # resources :sales do
54 | # get 'recent', on: :collection
55 | # end
56 | # end
57 |
58 | # Example resource route with concerns:
59 | # concern :toggleable do
60 | # post 'toggle'
61 | # end
62 | # resources :posts, concerns: :toggleable
63 | # resources :photos, concerns: :toggleable
64 |
65 | # Example resource route within a namespace:
66 | # namespace :admin do
67 | # # Directs /admin/products/* to Admin::ProductsController
68 | # # (app/controllers/admin/products_controller.rb)
69 | # resources :products
70 | # end
71 | end
72 |
--------------------------------------------------------------------------------
/app/controllers/bookings_controller.rb:
--------------------------------------------------------------------------------
1 | class BookingsController < ApplicationController
2 | def new
3 | end
4 |
5 | def now
6 | @booking = current_user.bookings.create(
7 | title: 'Get me a car now!',
8 | location: {lat: params[:lat], lon: params[:lon]},
9 | time: Time.now,
10 | asap: true
11 | )
12 |
13 | PerformBookings.execute(@booking)
14 |
15 | render json: {success: true}
16 | end
17 |
18 | def edit
19 | @booking = Booking.find(params[:id])
20 | end
21 |
22 | def cancel
23 | @booking = Booking.find(params[:id])
24 | if @booking.cancel!
25 | @booking.destroy
26 | render json: {success: true}
27 | else
28 | render json: {success: false}
29 | end
30 | end
31 |
32 | def destroy
33 | @booking = Booking.find(params[:id])
34 | render json: {success: !!@booking.destroy}
35 | end
36 |
37 | def create
38 | time = Chronic.parse("#{booking_params[:date]} at #{booking_params[:time]}")
39 |
40 | @booking = current_user.bookings.create(
41 | title: booking_params[:title],
42 | address: booking_params[:address],
43 | time: time
44 | )
45 |
46 | if @booking.persisted?
47 | render json: {success: true, id: @booking.id}
48 | else
49 | render json: {success: false}
50 | end
51 | end
52 |
53 | def update
54 | time = Chronic.parse("#{booking_params[:date]} at #{booking_params[:time]}")
55 | @booking = Booking.find(params[:id])
56 | @booking.update_attributes(
57 | title: booking_params[:title],
58 | address: booking_params[:address],
59 | time: time
60 | )
61 |
62 | if @booking.valid?
63 | render json: {success: true}
64 | else
65 | render json: {success: false}
66 | end
67 | end
68 |
69 | def upcoming
70 | @bookings = current_user.bookings.upcoming_or_active.order('time ASC')
71 | @bookings.to_a.map! { |b| BookingSerializer.new(b, root: false) }
72 | render json: @bookings
73 | end
74 |
75 | def past
76 | @bookings = current_user.bookings.past.order('time DESC')
77 | @bookings = @bookings.to_a - current_user.bookings.upcoming_or_active.order('time ASC').to_a
78 | @bookings.map! { |b| BookingSerializer.new(b, root: false) }
79 | render json: @bookings
80 | end
81 |
82 | private
83 |
84 | def booking_params
85 | params.require(:booking).permit(:title, :date, :time, :address)
86 | end
87 |
88 | end
89 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/8-circle.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | *
7 | *
8 | *
9 | *
10 | *
11 | *
12 | *
13 | *
14 | *
15 | *
16 | *
17 | *
18 | *
19 | */
20 | @import "../variables";
21 |
22 | .sk-spinner-circle {
23 | &.sk-spinner {
24 | margin: $spinkit-spinner-margin;
25 | width: 22px;
26 | height: 22px;
27 | position: relative;
28 | }
29 |
30 | .sk-circle {
31 | width: 100%;
32 | height: 100%;
33 | position: absolute;
34 | left: 0;
35 | top: 0;
36 | }
37 |
38 | .sk-circle:before {
39 | content: '';
40 | display: block;
41 | margin: 0 auto;
42 | width: 20%;
43 | height: 20%;
44 | background-color: $spinkit-spinner-color;
45 |
46 | border-radius: 100%;
47 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out;
48 | /* Prevent first frame from flickering when animation starts */
49 | animation-fill-mode: both;
50 | }
51 |
52 | .sk-circle2 { transform: rotate(30deg) }
53 | .sk-circle3 { transform: rotate(60deg) }
54 | .sk-circle4 { transform: rotate(90deg) }
55 | .sk-circle5 { transform: rotate(120deg) }
56 | .sk-circle6 { transform: rotate(150deg) }
57 | .sk-circle7 { transform: rotate(180deg) }
58 | .sk-circle8 { transform: rotate(210deg) }
59 | .sk-circle9 { transform: rotate(240deg) }
60 | .sk-circle10 { transform: rotate(270deg) }
61 | .sk-circle11 { transform: rotate(300deg) }
62 | .sk-circle12 { transform: rotate(330deg) }
63 |
64 | .sk-circle2:before { animation-delay: -1.1s }
65 | .sk-circle3:before { animation-delay: -1.0s }
66 | .sk-circle4:before { animation-delay: -0.9s }
67 | .sk-circle5:before { animation-delay: -0.8s }
68 | .sk-circle6:before { animation-delay: -0.7s }
69 | .sk-circle7:before { animation-delay: -0.6s }
70 | .sk-circle8:before { animation-delay: -0.5s }
71 | .sk-circle9:before { animation-delay: -0.4s }
72 | .sk-circle10:before { animation-delay: -0.3s }
73 | .sk-circle11:before { animation-delay: -0.2s }
74 | .sk-circle12:before { animation-delay: -0.1s }
75 | }
76 |
77 | @keyframes sk-circleBounceDelay {
78 | 0%, 80%, 100% { transform: scale(0.0) }
79 | 40% { transform: scale(1.0) }
80 | }
81 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spinkit/spinners/10-fading-circle.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Usage:
3 | *
4 | *
5 | *
6 | *
7 | *
8 | *
9 | *
10 | *
11 | *
12 | *
13 | *
14 | *
15 | *
16 | *
17 | *
18 | *
19 | */
20 | @import "../variables";
21 |
22 | .sk-spinner-fading-circle {
23 |
24 | &.sk-spinner {
25 | margin: $spinkit-spinner-margin;
26 | width: 22px;
27 | height: 22px;
28 | position: relative;
29 | }
30 |
31 | .sk-circle {
32 | width: 100%;
33 | height: 100%;
34 | position: absolute;
35 | left: 0;
36 | top: 0;
37 | }
38 |
39 | .sk-circle:before {
40 | content: '';
41 | display: block;
42 | margin: 0 auto;
43 | width: 18%;
44 | height: 18%;
45 | background-color: $spinkit-spinner-color;
46 |
47 | border-radius: 100%;
48 | animation: sk-circleFadeDelay 1.2s infinite ease-in-out;
49 | /* Prevent first frame from flickering when animation starts */
50 | animation-fill-mode: both;
51 | }
52 |
53 | .sk-circle2 { transform: rotate(30deg) }
54 | .sk-circle3 { transform: rotate(60deg) }
55 | .sk-circle4 { transform: rotate(90deg) }
56 | .sk-circle5 { transform: rotate(120deg) }
57 | .sk-circle6 { transform: rotate(150deg) }
58 | .sk-circle7 { transform: rotate(180deg) }
59 | .sk-circle8 { transform: rotate(210deg) }
60 | .sk-circle9 { transform: rotate(240deg) }
61 | .sk-circle10 { transform: rotate(270deg) }
62 | .sk-circle11 { transform: rotate(300deg) }
63 | .sk-circle12 { transform: rotate(330deg) }
64 |
65 | .sk-circle2:before { animation-delay: -1.1s }
66 | .sk-circle3:before { animation-delay: -1.0s }
67 | .sk-circle4:before { animation-delay: -0.9s }
68 | .sk-circle5:before { animation-delay: -0.8s }
69 | .sk-circle6:before { animation-delay: -0.7s }
70 | .sk-circle7:before { animation-delay: -0.6s }
71 | .sk-circle8:before { animation-delay: -0.5s }
72 | .sk-circle9:before { animation-delay: -0.4s }
73 | .sk-circle10:before { animation-delay: -0.3s }
74 | .sk-circle11:before { animation-delay: -0.2s }
75 | .sk-circle12:before { animation-delay: -0.1s }
76 |
77 | }
78 |
79 | @keyframes sk-circleFadeDelay {
80 | 0%, 39%, 100% { opacity: 0 }
81 | 40% { opacity: 1 }
82 | }
83 |
--------------------------------------------------------------------------------
/app/models/booking.rb:
--------------------------------------------------------------------------------
1 | class Booking < ActiveRecord::Base
2 | belongs_to :user
3 | validates :address, :lat, :lon, :time, :title, presence: true
4 |
5 | def self.upcoming
6 | where('time > ?', Time.now)
7 | end
8 |
9 | def self.incomplete
10 | where('complete = ?', false)
11 | end
12 |
13 | def self.upcoming_or_active
14 | where('time > ? or
15 | (complete = ? and car_booked_time > ?) or
16 | (asap = ? and complete = ?)',
17 | Time.now,
18 | true, Time.now - 30.minutes,
19 | true, false)
20 | end
21 |
22 | def self.within_booking_window
23 | where('(time >= ? and time <= ?) or (asap = ?)',
24 | Time.now, Time.now + 25.minutes, true)
25 | end
26 |
27 | def self.complete
28 | where('time < ? and complete = ?', Time.now, true)
29 | end
30 |
31 | def self.past
32 | where('time < ?', Time.now)
33 | end
34 |
35 | def self.past_and_incomplete
36 | where('time < ? and complete = ?', Time.now, false)
37 | end
38 |
39 | def cancel!
40 | return false if complete? == false || reservation_id.blank?
41 |
42 | if user.cancel_booking(reservation_id)
43 | clear_booking_attributes!
44 | true
45 | end
46 | end
47 |
48 | def clear_booking_attributes!
49 | update_attributes(complete: false,
50 | in_progress: false,
51 | car_address: nil,
52 | car_booked_time: nil,
53 | car_license_plate: nil,
54 | reservation_id: nil,
55 | reservation_response: nil)
56 | end
57 |
58 | def perform!(vin)
59 | if car = user.create_booking(vin)
60 | update_attributes(complete: true,
61 | in_progress: false,
62 | car_address: car[:address],
63 | car_booked_time: car[:time],
64 | car_license_plate: car[:license_plate],
65 | reservation_id: car[:reservation_id],
66 | reservation_response: car[:full_response])
67 | end
68 | end
69 |
70 | def address=(new_address)
71 | if new_address != address
72 | write_attribute(:address, new_address)
73 | update_lat_lon_from_address
74 | end
75 | end
76 |
77 | def location=(coords)
78 | write_attribute(:lat, coords[:lat])
79 | write_attribute(:lon, coords[:lon])
80 | write_attribute(:address, 'your current location')
81 | end
82 |
83 | def distance_to(other_location)
84 | location.distance_to(other_location)
85 | end
86 |
87 | def location
88 | Geokit::GeoLoc.new(lat: lat, lng: lon)
89 | end
90 |
91 | def update_lat_lon_from_address
92 | result = Geokit::Geocoders::GoogleGeocoder.geocode(address)
93 | if result.success?
94 | self.lat = result.lat
95 | self.lon = result.lng
96 | else
97 | self.lat = 0
98 | self.lon = 0
99 | end
100 | end
101 |
102 | end
103 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/booking_form.js.jsx:
--------------------------------------------------------------------------------
1 | global.BookingForm = React.createClass({
2 | mixins: [React.addons.LinkedStateMixin],
3 |
4 | getDefaultProps: function() {
5 | return { booking: {}, isLoading: false }
6 | },
7 |
8 | getInitialState: function() {
9 | var booking = this.props.booking;
10 |
11 | return {
12 | title: booking.title || '',
13 | date: booking.date || '',
14 | time: booking.time || '',
15 | address: booking.address || ''
16 | }
17 | },
18 |
19 | submit: function(e) {
20 | e.preventDefault();
21 |
22 | var booking = {
23 | title: this.state.title, date: this.state.date,
24 | time: this.state.time, address: this.state.address
25 | };
26 |
27 | this.props.onSubmit(booking);
28 | },
29 |
30 | getCurrentLocation: function(e) {
31 | e.preventDefault();
32 | this.setState({isLoading: true});
33 | var self = this;
34 |
35 | var failedToFindLocation = function() {
36 | alert("Sorry, your location could not be found");
37 | };
38 |
39 | var setAddressToCurrent = function(position) {
40 | var data = {lat: position.coords.latitude, lon: position.coords.longitude}
41 |
42 | ApiClient.addressForCoords(data, function(result) {
43 | self.setState({isLoading: false});
44 |
45 | if (result.error) {
46 | failedToFindLocation();
47 | } else {
48 | self.setState({address: result.address});
49 | }
50 | });
51 | };
52 |
53 | navigator.geolocation.getCurrentPosition(setAddressToCurrent, failedToFindLocation, {
54 | maximumAge: 60000, enableHighAccuracy: true
55 | });
56 | },
57 |
58 | render: function() {
59 | return (
60 |
86 | )
87 | }
88 | });
89 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 8.2 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On OS X with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On OS X with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem 'pg'
16 | #
17 | default: &default
18 | adapter: postgresql
19 | encoding: unicode
20 | # For details on connection pooling, see rails configuration guide
21 | # http://guides.rubyonrails.org/configuring.html#database-pooling
22 | pool: 5
23 |
24 | development:
25 | <<: *default
26 | database: concierge_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: concierge
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: concierge_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: concierge_production
84 | username: concierge
85 | password: <%= ENV['CONCIERGE_DATABASE_PASSWORD'] %>
86 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like
20 | # NGINX, varnish or squid.
21 | # config.action_dispatch.rack_cache = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
26 |
27 | # Compress JavaScripts and CSS.
28 | config.assets.js_compressor = :uglifier
29 | # config.assets.css_compressor = :sass
30 |
31 | # Do not fallback to assets pipeline if a precompiled asset is missed.
32 | config.assets.compile = false
33 |
34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
35 | # yet still be able to expire them through the digest params.
36 | config.assets.digest = true
37 |
38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
39 |
40 | # Specifies the header that your server uses for sending files.
41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
43 |
44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
45 | # config.force_ssl = true
46 |
47 | # Use the lowest log level to ensure availability of diagnostic information
48 | # when problems arise.
49 | config.log_level = :debug
50 |
51 | # Prepend all log lines with the following tags.
52 | # config.log_tags = [ :subdomain, :uuid ]
53 |
54 | # Use a different logger for distributed setups.
55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
56 |
57 | # Use a different cache store in production.
58 | config.cache_store = :dalli_store,
59 | (ENV["MEMCACHIER_SERVERS"] || "").split(","),
60 | {username: ENV["MEMCACHIER_USERNAME"],
61 | password: ENV["MEMCACHIER_PASSWORD"],
62 | failover: true,
63 | socket_timeout: 1.5,
64 | socket_failure_delay: 0.2 }
65 |
66 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
67 | # config.action_controller.asset_host = 'http://assets.example.com'
68 |
69 | # Ignore bad email addresses and do not raise email delivery errors.
70 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
71 | # config.action_mailer.raise_delivery_errors = false
72 |
73 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
74 | # the I18n.default_locale when a translation cannot be found).
75 | config.i18n.fallbacks = true
76 |
77 | # Send deprecation notices to registered listeners.
78 | config.active_support.deprecation = :notify
79 |
80 | # Use default logging formatter so that PID and timestamp are not suppressed.
81 | config.log_formatter = ::Logger::Formatter.new
82 |
83 | # Do not dump schema after migrations.
84 | config.active_record.dump_schema_after_migration = false
85 | end
86 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/list_page.js.jsx:
--------------------------------------------------------------------------------
1 | global.ListPage = React.createClass({
2 | mixins: [HasTimer],
3 |
4 | getInitialState: function() {
5 | return {
6 | upcomingBookings: this.props.upcomingBookings,
7 | pastBookings: [],
8 | isLoading: false,
9 | view: 'upcoming'
10 | }
11 | },
12 |
13 | componentDidMount: function() {
14 | this.loadBookings();
15 | this.setInterval(this.loadBookings, 45000);
16 | },
17 |
18 | showPastBookings: function() {
19 | this.setState({view: 'past'});
20 | },
21 |
22 | showUpcomingBookings: function() {
23 | this.setState({view: 'upcoming'});
24 | },
25 |
26 | cancelBookingFn: function(booking) {
27 | var self = this;
28 |
29 | return function(e) {
30 | e.preventDefault()
31 | self.setState({isLoading: true});
32 |
33 | $.ajax({
34 | url: '/bookings/' + booking.id,
35 | type: 'DELETE',
36 | success: function(result) {
37 | // Lazy
38 | window.location.reload();
39 | },
40 | complete: function() {
41 | // self.setState({isLoading: false});
42 | }
43 | });
44 | }
45 | },
46 |
47 | loadBookings: function() {
48 | var self = this;
49 |
50 | $.get('/bookings/upcoming', function(response) {
51 | self.setState({upcomingBookings: response})
52 | })
53 |
54 | $.get('/bookings/past', function(response) {
55 | self.setState({pastBookings: response})
56 | })
57 | },
58 |
59 | renderBookings: function(bookings) {
60 | var list = [], self = this;
61 |
62 | if (bookings == 0) {
63 | return (No {this.state.view} bookings!
)
64 | }
65 |
66 | bookings.forEach(function(b) {
67 | var actions,
68 | location = (Near {b.address} ),
69 | badge;
70 |
71 | if (b.inProgress == true && b.complete == false) {
72 | badge = (
73 | Searching...
74 | )
75 | }
76 |
77 | if (b.complete == true) {
78 | actions = (
79 |
80 | )
81 |
82 | location = (
83 |
84 |
85 | {b.carLicensePlate} at
86 |
88 | {b.carAddress}
89 |
90 |
91 | )
92 | } else if (b.complete == false) {
93 | actions = (
94 |
95 | )
96 | }
97 |
98 | list.push(
99 |
100 |
101 |
{b.title}{badge}
102 |
103 |
104 | {b.time}
105 |
106 |
107 | ,
108 |
109 |
110 | {b.day}, {b.date}
111 |
112 |
113 |
114 |
115 | {location}
116 |
117 |
118 |
119 |
120 | {actions}
121 |
122 |
123 | )
124 | });
125 |
126 | return list;
127 | },
128 |
129 | render: function() {
130 | var cx = React.addons.classSet, bookings,
131 | upcomingClasses = cx({'page-subtitle--link': true,
132 | 'active': this.state.view == 'upcoming'}),
133 | pastClasses = cx({'page-subtitle--link': true,
134 | 'active': this.state.view == 'past'});
135 |
136 | if (this.state.view == 'upcoming') {
137 | bookings = this.state.upcomingBookings;
138 | } else {
139 | bookings = this.state.pastBookings;
140 | }
141 |
142 | return (
143 |
144 |
145 |
146 |
147 |
148 |
149 | Upcoming
151 |
152 | Past
154 |
155 |
156 | {this.renderBookings(bookings)}
157 |
158 |
159 |
160 | )
161 | }
162 | });
163 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/reactjs/react-rails.git
3 | revision: abd5171863bef2b426b0d6ca1354ec55679ed15a
4 | specs:
5 | react-rails (1.0.0.pre)
6 | coffee-script-source (~> 1.9)
7 | connection_pool
8 | execjs
9 | rails (>= 3.1)
10 | react-source (~> 0.13)
11 |
12 | GEM
13 | remote: https://rubygems.org/
14 | specs:
15 | actionmailer (4.2.0)
16 | actionpack (= 4.2.0)
17 | actionview (= 4.2.0)
18 | activejob (= 4.2.0)
19 | mail (~> 2.5, >= 2.5.4)
20 | rails-dom-testing (~> 1.0, >= 1.0.5)
21 | actionpack (4.2.0)
22 | actionview (= 4.2.0)
23 | activesupport (= 4.2.0)
24 | rack (~> 1.6.0)
25 | rack-test (~> 0.6.2)
26 | rails-dom-testing (~> 1.0, >= 1.0.5)
27 | rails-html-sanitizer (~> 1.0, >= 1.0.1)
28 | actionview (4.2.0)
29 | activesupport (= 4.2.0)
30 | builder (~> 3.1)
31 | erubis (~> 2.7.0)
32 | rails-dom-testing (~> 1.0, >= 1.0.5)
33 | rails-html-sanitizer (~> 1.0, >= 1.0.1)
34 | active_model_serializers (0.9.2)
35 | activemodel (>= 3.2)
36 | activejob (4.2.0)
37 | activesupport (= 4.2.0)
38 | globalid (>= 0.3.0)
39 | activemodel (4.2.0)
40 | activesupport (= 4.2.0)
41 | builder (~> 3.1)
42 | activerecord (4.2.0)
43 | activemodel (= 4.2.0)
44 | activesupport (= 4.2.0)
45 | arel (~> 6.0)
46 | activesupport (4.2.0)
47 | i18n (~> 0.7)
48 | json (~> 1.7, >= 1.7.7)
49 | minitest (~> 5.1)
50 | thread_safe (~> 0.3, >= 0.3.4)
51 | tzinfo (~> 1.1)
52 | arel (6.0.0)
53 | autoprefixer-rails (5.1.0)
54 | execjs
55 | json
56 | bcrypt (3.1.9)
57 | binding_of_caller (0.7.2)
58 | debug_inspector (>= 0.0.1)
59 | builder (3.2.2)
60 | byebug (4.0.3)
61 | columnize (= 0.9.0)
62 | chronic (0.10.2)
63 | clockwork (1.1.0)
64 | activesupport
65 | tzinfo
66 | coffee-rails (4.1.0)
67 | coffee-script (>= 2.2.0)
68 | railties (>= 4.0.0, < 5.0)
69 | coffee-script (2.3.0)
70 | coffee-script-source
71 | execjs
72 | coffee-script-source (1.9.1)
73 | columnize (0.9.0)
74 | connection_pool (2.1.3)
75 | dalli (2.7.4)
76 | debug_inspector (0.0.2)
77 | dotenv (1.0.2)
78 | dotenv-rails (1.0.2)
79 | dotenv (= 1.0.2)
80 | erubis (2.7.0)
81 | execjs (2.4.0)
82 | geokit (1.9.0)
83 | multi_json (>= 1.3.2)
84 | globalid (0.3.3)
85 | activesupport (>= 4.1.0)
86 | hike (1.2.3)
87 | httparty (0.13.3)
88 | json (~> 1.8)
89 | multi_xml (>= 0.5.2)
90 | i18n (0.7.0)
91 | jbuilder (2.2.12)
92 | activesupport (>= 3.0.0, < 5)
93 | multi_json (~> 1.2)
94 | jquery-rails (4.0.3)
95 | rails-dom-testing (~> 1.0)
96 | railties (>= 4.2.0)
97 | thor (>= 0.14, < 2.0)
98 | json (1.8.2)
99 | jwt (1.4.1)
100 | kgio (2.9.3)
101 | loofah (2.0.1)
102 | nokogiri (>= 1.5.9)
103 | mail (2.6.3)
104 | mime-types (>= 1.16, < 3)
105 | mime-types (2.4.3)
106 | mini_portile (0.6.2)
107 | minitest (5.5.1)
108 | multi_json (1.11.0)
109 | multi_xml (0.5.5)
110 | nokogiri (1.6.6.2)
111 | mini_portile (~> 0.6.0)
112 | pg (0.18.1)
113 | rack (1.6.0)
114 | rack-test (0.6.3)
115 | rack (>= 1.0)
116 | rails (4.2.0)
117 | actionmailer (= 4.2.0)
118 | actionpack (= 4.2.0)
119 | actionview (= 4.2.0)
120 | activejob (= 4.2.0)
121 | activemodel (= 4.2.0)
122 | activerecord (= 4.2.0)
123 | activesupport (= 4.2.0)
124 | bundler (>= 1.3.0, < 2.0)
125 | railties (= 4.2.0)
126 | sprockets-rails
127 | rails-deprecated_sanitizer (1.0.3)
128 | activesupport (>= 4.2.0.alpha)
129 | rails-dom-testing (1.0.6)
130 | activesupport (>= 4.2.0.beta, < 5.0)
131 | nokogiri (~> 1.6.0)
132 | rails-deprecated_sanitizer (>= 1.0.1)
133 | rails-html-sanitizer (1.0.2)
134 | loofah (~> 2.0)
135 | rails_12factor (0.0.3)
136 | rails_serve_static_assets
137 | rails_stdout_logging
138 | rails_serve_static_assets (0.0.2)
139 | rails_stdout_logging (0.0.3)
140 | railties (4.2.0)
141 | actionpack (= 4.2.0)
142 | activesupport (= 4.2.0)
143 | rake (>= 0.8.7)
144 | thor (>= 0.18.1, < 2.0)
145 | raindrops (0.13.0)
146 | rake (10.4.2)
147 | react-source (0.13.1)
148 | sass (3.4.13)
149 | sass-rails (5.0.1)
150 | railties (>= 4.0.0, < 5.0)
151 | sass (~> 3.1)
152 | sprockets (>= 2.8, < 4.0)
153 | sprockets-rails (>= 2.0, < 4.0)
154 | tilt (~> 1.1)
155 | sprockets (2.12.3)
156 | hike (~> 1.2)
157 | multi_json (~> 1.0)
158 | rack (~> 1.0)
159 | tilt (~> 1.1, != 1.3.0)
160 | sprockets-rails (2.2.4)
161 | actionpack (>= 3.0)
162 | activesupport (>= 3.0)
163 | sprockets (>= 2.8, < 4.0)
164 | thor (0.19.1)
165 | thread_safe (0.3.5)
166 | tilt (1.4.1)
167 | twilio-ruby (3.15.2)
168 | builder (>= 2.1.2)
169 | jwt (~> 1.0)
170 | multi_json (>= 1.3.0)
171 | tzinfo (1.2.2)
172 | thread_safe (~> 0.1)
173 | uglifier (2.7.1)
174 | execjs (>= 0.3.0)
175 | json (>= 1.8.0)
176 | unicorn (4.8.3)
177 | kgio (~> 2.6)
178 | rack
179 | raindrops (~> 0.7)
180 | web-console (2.1.2)
181 | activemodel (>= 4.0)
182 | binding_of_caller (>= 0.7.2)
183 | railties (>= 4.0)
184 | sprockets-rails (>= 2.0, < 4.0)
185 |
186 | PLATFORMS
187 | ruby
188 |
189 | DEPENDENCIES
190 | active_model_serializers
191 | autoprefixer-rails
192 | bcrypt (~> 3.1.7)
193 | byebug
194 | chronic
195 | clockwork
196 | coffee-rails (~> 4.1.0)
197 | dalli
198 | dotenv-rails
199 | geokit
200 | httparty
201 | jbuilder (~> 2.0)
202 | jquery-rails
203 | pg
204 | rails (= 4.2.0)
205 | rails_12factor
206 | react-rails (~> 1.0.0.pre)!
207 | sass-rails (~> 5.0)
208 | twilio-ruby
209 | uglifier (>= 1.3.0)
210 | unicorn
211 | web-console (~> 2.0)
212 |
--------------------------------------------------------------------------------
/app/assets/javascripts/components/login_page.js.jsx:
--------------------------------------------------------------------------------
1 | global.LoginPage = React.createClass({
2 | getInitialState: function() {
3 | return {
4 | waitingForVerifier: false,
5 | waitingForUserInfo: false,
6 | showExistingAccountForm: false,
7 | isLoading: false
8 | }
9 | },
10 |
11 | toggleVerifierInput: function(e) {
12 | this.setState({waitingForVerifier: true});
13 | },
14 |
15 | toggleExistingAccountForm: function(e) {
16 | e.preventDefault();
17 | this.setState({showExistingAccountForm: !this.state.showExistingAccountForm});
18 | requestAnimationFrame(function() {
19 | $(window).scrollTop(0);
20 | });
21 | },
22 |
23 | submitExistingAccountForm: function(e) {
24 | var email = this.refs.emailInput.getDOMNode().value,
25 | password = this.refs.passwordInput.getDOMNode().value,
26 | self = this;
27 |
28 | e.preventDefault();
29 |
30 | this.setState({isLoading: true});
31 |
32 | $.post('/sessions', {email: email, password: password}).
33 | done(function(response) {
34 | if (response.success == true) {
35 | window.location.href = '/';
36 | } else {
37 | alert('Invalid username or password');
38 | }
39 | }).
40 | complete(function() {
41 | self.setState({isLoading: false});
42 | });
43 | },
44 |
45 | renderExistingAccountForm: function() {
46 | return (
47 |
61 | )
62 | },
63 |
64 | submitVerificationCode: function(e) {
65 | e.preventDefault();
66 | var verificationCode = this.refs.verifierInput.getDOMNode().value,
67 | self = this;
68 |
69 | this.setState({isLoading: true});
70 |
71 | $.post('/auth', {verificationCode: verificationCode}).
72 | done(function(response) {
73 | if (response.success == true) {
74 | self.setState({waitingForVerifier: false, waitingForUserInfo: true});
75 | } else {
76 | alert('Hmm, that did not seem to work. Did you copy the code properly?')
77 | }
78 | self.setState({isLoading: false});
79 | });
80 | },
81 |
82 | submitUserInfo: function(e) {
83 | e.preventDefault()
84 |
85 | var name = this.refs.nameInput.getDOMNode().value,
86 | email = this.refs.emailInput.getDOMNode().value,
87 | phone = this.refs.phoneInput.getDOMNode().value,
88 | password = this.refs.passwordInput.getDOMNode().value,
89 | self = this;
90 |
91 | self.setState({isLoading: true});
92 |
93 | $.post('/users', {user: {name: name, email: email, phone: phone, password: password}}).
94 | done(function(response) {
95 | if (response.success == true) {
96 | window.location.reload();
97 | } else {
98 | alert('Oops, no luck. Did you fill everything in? All fields are required!')
99 | self.setState({isLoading: false});
100 | }
101 | });
102 | },
103 |
104 | render: function() {
105 | var action;
106 |
107 | if (this.state.waitingForVerifier) {
108 | action = (
109 |
117 | )
118 | } else if (this.state.waitingForUserInfo) {
119 | action = (
120 |
136 | )
137 | } else if (this.state.showExistingAccountForm) {
138 | action = this.renderExistingAccountForm();
139 | } else {
140 | action = (
141 |
161 | )
162 | }
163 | return (
164 |
165 |
166 |
167 |
168 |
Car2Go Concierge
169 |
170 | {action}
171 |
172 |
173 |
174 | )
175 | },
176 |
177 | componentDidUpdate: function(prevProps, prevState) {
178 | if (prevState.waitingForVerifier === false &&
179 | this.state.waitingForVerifier === true) {
180 | $(this.refs.verifierInput.getDOMNode()).focus();
181 | }
182 |
183 | if (prevState.waitingForUserInfo === false &&
184 | this.state.waitingForUserInfo === true) {
185 | $(this.refs.nameInput.getDOMNode()).focus();
186 | }
187 |
188 | if (prevState.showExistingAccountForm === false &&
189 | this.state.showExistingAccountForm === true) {
190 | $(this.refs.emailInput.getDOMNode()).focus();
191 | }
192 | }
193 | })
194 |
--------------------------------------------------------------------------------
/app/models/api_client.rb:
--------------------------------------------------------------------------------
1 | # TODO: Should have retries
2 | class ApiClient
3 | OAUTH_CONSUMER_KEY = ENV['CAR2GO_CONSUMER_KEY']
4 | OAUTH_SECRET_KEY = ENV['CAR2GO_SECRET_KEY']
5 | OAUTH_VERSION = '1.0'
6 | OAUTH_SIGNATURE_METHOD = 'HMAC-SHA1'
7 |
8 | def self.available_cars(loc = 'vancouver')
9 | url = "http://www.car2go.com/api/v2.1/vehicles?loc=#{loc}&oauth_consumer_key=#{OAUTH_CONSUMER_KEY}&format=json"
10 | HTTParty.get(url).parsed_response['placemarks']
11 | end
12 |
13 | def self.authorization_url(store = {})
14 | new.authorization_url(store)
15 | end
16 |
17 | def initialize(token_secret = nil, token = nil)
18 | @token_secret = token_secret
19 | @token = token
20 | end
21 |
22 | # Returns {oauth_token: '...', oauth_token_secret: '...'}
23 | # which can be used to generate an authorization URL
24 | #
25 | def get_request_tokens
26 | response = post('https://www.car2go.com/api/reqtoken').parsed_response
27 | Rack::Utils.parse_nested_query(response).symbolize_keys!
28 | end
29 |
30 | # Returns {oauth_token: '...', oauth_token_secret: '...'}
31 | # which can be used for user-specific API requests
32 | #
33 | def get_access_tokens(verification_code)
34 | @verifier = verification_code
35 | url = 'https://www.car2go.com/api/accesstoken'
36 | headers = {'Authorization' => auth_headers(url, :for_authentication)}
37 | response = HTTParty.post(url, headers: headers).parsed_response
38 | Rack::Utils.parse_nested_query(response).symbolize_keys!
39 | end
40 |
41 | def authorization_url(store = {})
42 | tokens = get_request_tokens
43 | store[:oauth_token] = tokens[:oauth_token]
44 | store[:oauth_token_secret] = tokens[:oauth_token_secret]
45 | "https://www.car2go.com/api/authorize?oauth_token=#{tokens[:oauth_token]}"
46 | end
47 |
48 | # TODO: Need to handle error cases!
49 | # http://url.brentvatne.ca/12FjM
50 | #
51 | def create_booking(vin, account)
52 | url = 'https://www.car2go.com/api/v2.1/bookings'
53 | response = HTTParty.post(
54 | url + "?format=json&loc=vancouver&vin=#{vin}&account=#{account}",
55 | headers: {'Authorization' => auth_headers(url, :create_booking, vin: vin, account: account)}
56 | )
57 |
58 |
59 | response = response.parsed_response['booking'].first
60 |
61 | {address: response['bookingposition']['address'],
62 | time: Time.at(response['reservationTime']['timeInMillis'] / 1000),
63 | license_plate: response['vehicle']['numberPlate'],
64 | reservation_id: response['bookingId'],
65 | full_response: response }
66 | end
67 |
68 | # Returns true if successful, false otherwise
69 | def cancel_booking(id)
70 | url = "https://www.car2go.com/api/v2.1/booking/#{id}"
71 | response = HTTParty.delete(url,
72 | headers: {'Authorization' => auth_headers(url, :cancel_booking, booking_id: id)})
73 |
74 | response.parsed_response['cancelBookingResponse']['returnValue']['code'] == '0'
75 | end
76 |
77 | def rentals
78 | get('https://www.car2go.com/api/v2.1/rentals')['rentals']
79 | end
80 |
81 | def accounts
82 | get('https://www.car2go.com/api/v2.1/accounts').parsed_response['account']
83 | end
84 |
85 | private
86 |
87 | def get(url)
88 | HTTParty.get(url + "?format=json&loc=vancouver",
89 | headers: {'Authorization' => auth_headers(url)})
90 | end
91 |
92 | def post(url)
93 | HTTParty.get(url + "?format=json&loc=vancouver",
94 | headers: {'Authorization' => auth_headers(url)})
95 | end
96 |
97 | def oauth_timestamp
98 | @oauth_timestamp ||= Time.now.to_i.to_s
99 | end
100 |
101 | def oauth_nonce
102 | @oauth_nonce ||= Random.rand(100000).to_s
103 | end
104 |
105 | def oauth_token
106 | @token || ''
107 | end
108 |
109 | def oauth_token_secret
110 | @token_secret || ''
111 | end
112 |
113 | def base_auth_headers
114 | 'OAuth oauth_signature_method="HMAC-SHA1", ' +
115 | 'oauth_consumer_key="' + OAUTH_CONSUMER_KEY + '", ' +
116 | 'oauth_version="1.0", ' +
117 | 'oauth_timestamp="' + oauth_timestamp + '", ' +
118 | 'oauth_nonce="' + oauth_nonce + '", ' +
119 | 'oauth_callback="oob", ' +
120 | 'oauth_token="' + oauth_token + '"'
121 | end
122 |
123 | def base_auth_headers_with_verifier
124 | base_auth_headers + ', ' +
125 | 'oauth_verifier="' + @verifier + '"'
126 | end
127 |
128 | def parameters
129 | 'format=json' +
130 | '&loc=vancouver' +
131 | '&oauth_callback=' +
132 | 'oob' +
133 | '&oauth_consumer_key=' +
134 | OAUTH_CONSUMER_KEY +
135 | '&oauth_nonce=' +
136 | oauth_nonce +
137 | '&oauth_signature_method=' +
138 | OAUTH_SIGNATURE_METHOD +
139 | '&oauth_timestamp=' +
140 | oauth_timestamp +
141 | '&oauth_token=' +
142 | oauth_token +
143 | '&oauth_version=' +
144 | OAUTH_VERSION
145 | end
146 |
147 | def parameters_for_cancel(booking_id)
148 | # 'bookingId=' + booking_id.to_s +
149 | 'oauth_callback=' +
150 | 'oob' +
151 | '&oauth_consumer_key=' +
152 | OAUTH_CONSUMER_KEY +
153 | '&oauth_nonce=' +
154 | oauth_nonce +
155 | '&oauth_signature_method=' +
156 | OAUTH_SIGNATURE_METHOD +
157 | '&oauth_timestamp=' +
158 | oauth_timestamp +
159 | '&oauth_token=' +
160 | oauth_token +
161 | '&oauth_version=' +
162 | OAUTH_VERSION
163 | end
164 |
165 | def parameters_for_booking(vin, account)
166 | 'account=' + account.to_s +
167 | '&format=json' +
168 | '&loc=vancouver' +
169 | '&oauth_callback=' +
170 | 'oob' +
171 | '&oauth_consumer_key=' +
172 | OAUTH_CONSUMER_KEY +
173 | '&oauth_nonce=' +
174 | oauth_nonce +
175 | '&oauth_signature_method=' +
176 | OAUTH_SIGNATURE_METHOD +
177 | '&oauth_timestamp=' +
178 | oauth_timestamp +
179 | '&oauth_token=' +
180 | oauth_token +
181 | '&oauth_version=' +
182 | OAUTH_VERSION +
183 | '&vin=' +
184 | vin
185 | end
186 |
187 | def auth_parameters
188 | 'oauth_callback=' +
189 | 'oob' +
190 | '&oauth_consumer_key=' +
191 | OAUTH_CONSUMER_KEY +
192 | '&oauth_nonce=' +
193 | oauth_nonce +
194 | '&oauth_signature_method=' +
195 | OAUTH_SIGNATURE_METHOD +
196 | '&oauth_timestamp=' +
197 | oauth_timestamp +
198 | '&oauth_token=' +
199 | oauth_token +
200 | '&oauth_verifier=' +
201 | @verifier +
202 | '&oauth_version=' +
203 | OAUTH_VERSION
204 | end
205 |
206 | def auth_headers(url, type = :normal, options = {})
207 | signature = nil
208 | headers = nil
209 |
210 | if type == :for_authentication
211 | signature = oauth_signature(url, 'POST', auth_parameters)
212 | headers = base_auth_headers_with_verifier
213 | elsif type == :create_booking
214 | signature = oauth_signature(url, 'POST',
215 | parameters_for_booking(options[:vin], options[:account]))
216 | headers = base_auth_headers
217 | elsif type == :cancel_booking
218 | signature = oauth_signature(url, 'DELETE', parameters_for_cancel(options[:booking_id]))
219 | headers = base_auth_headers
220 | else
221 | signature = oauth_signature(url)
222 | headers = base_auth_headers
223 | end
224 |
225 | headers + ', oauth_signature="' + signature + '"'
226 | end
227 |
228 | def base_string(url, request_type="GET", params = parameters)
229 | request_type + '&' + CGI.escape(url) + '&' + CGI.escape(params)
230 | end
231 |
232 | def oauth_signature(url, request_type="GET", params = parameters)
233 | CGI.escape(Base64.encode64(
234 | "#{OpenSSL::HMAC.digest('sha1', OAUTH_SECRET_KEY + "&" +
235 | oauth_token_secret, base_string(url, request_type, params))}").chomp)
236 | end
237 | end
238 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | *= require spinkit/spinkit
16 | */
17 |
18 | $mobile-bp: 500px;
19 |
20 | @mixin mobile {
21 | @media (max-width: $mobile-bp) {
22 | @content;
23 | }
24 | }
25 |
26 | @mixin desktop {
27 | @media (min-width: $mobile-bp) {
28 | @content;
29 | }
30 | }
31 |
32 | html {
33 | box-sizing: border-box;
34 | margin: 0;
35 | padding: 0;
36 | }
37 |
38 | *, *:before, *:after {
39 | box-sizing: inherit;
40 | }
41 |
42 | body {
43 | background: #F5F2F2;
44 | margin: 0;
45 | padding: 0;
46 | }
47 |
48 | h1,h2,h3,h4,h5,h6,p,span,div,input {
49 | font-family: 'Open Sans', sans-serif;
50 | }
51 |
52 | .container {
53 | position: relative;
54 | max-width: 1140px;
55 | margin: 0 auto;
56 | }
57 |
58 | .card {
59 | padding: 20px;
60 | background: #fff;
61 | box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.14);
62 | margin: 0 auto;
63 | text-align: center;
64 | position: relative;
65 |
66 | p {
67 | @include desktop {
68 | max-width: 75%;
69 | }
70 | margin: 0 auto 30px auto;
71 | }
72 |
73 | h1 {
74 | margin-bottom: 30px;
75 | }
76 | }
77 |
78 | .login-box {
79 | @include desktop {
80 | margin-top: 30px;
81 | }
82 |
83 | @include mobile {
84 | margin-top: 0;
85 | }
86 | }
87 |
88 | $blue: #4A90E2;
89 |
90 | .big-button {
91 | border: none;
92 | display: inline-block;
93 | width: 100%;
94 | background: $blue;
95 | padding: 20px;
96 | color: #fff;
97 | text-decoration: none;
98 | font-weight: bold;
99 |
100 | @include desktop {
101 | font-size: 18px;
102 | }
103 |
104 | @include mobile {
105 | font-size: 16px;
106 | }
107 |
108 | &:hover {
109 | cursor: pointer;
110 | }
111 | }
112 |
113 | .alternate-button {
114 | background: rgb(98, 105, 117);
115 | }
116 |
117 | .mb20 {
118 | margin-bottom: 20px;
119 | }
120 |
121 | .medium-button {
122 | border: none;
123 | display: inline-block;
124 | background: $blue;
125 | padding: 15px 40px;
126 | color: #fff;
127 | font-size: 17px;
128 | text-decoration: none;
129 | font-weight: bold;
130 | margin-right: 20px;
131 |
132 | &:hover {
133 | cursor: pointer;
134 | }
135 | }
136 |
137 | .cancel-button {
138 | background: #BFBFBF;
139 | }
140 |
141 | .footer {
142 | margin-top: 20px;
143 | color: rgba(0,0,0,0.35);
144 | text-align: center;
145 | font-weight: 300;
146 | margin-bottom: 15px;
147 |
148 | @include mobile {
149 | font-size: 14px;
150 | }
151 | }
152 |
153 | .login-box {
154 | max-width: 650px;
155 | }
156 |
157 | .login-box, .new-booking-form {
158 | input[type=text], input[type=email], input[type=password] {
159 | width: 100%;
160 | font-size: 16px;
161 | padding: 10px 15px 15px 15px;
162 | margin-bottom: 15px;
163 | border: 1px solid #eee;
164 |
165 | @include mobile {
166 | font-size: 14px;
167 | padding: 10px;
168 | margin-bottom: 10px;
169 | }
170 | }
171 | }
172 |
173 | .new-booking-form {
174 | padding: 20px 30px 30px 30px;
175 | padding-bottom: 0px;
176 |
177 | @include mobile {
178 | padding: 15px;
179 | padding-bottom: 0;
180 | }
181 |
182 | input[type=text] {
183 | padding: 20px 20px;
184 | background: #FEFEFE;
185 | border: 1px solid #F1F1F1;
186 | font-weight: 300;
187 |
188 | @include mobile {
189 | padding: 15px 10px;
190 | }
191 | }
192 | }
193 |
194 | .verifier-input-wrapper {
195 | img {
196 | max-width: 100%;
197 | }
198 |
199 | input[type=text] {
200 | box-shadow: 0px 0px 10px 3px rgba(224, 231, 14, 0.3);
201 | border: 1px solid #DAD10B;
202 | text-align: center;
203 |
204 | &:focus {
205 | outline: none;
206 | }
207 | }
208 | }
209 |
210 | .primary-navigation {
211 | background: #4A90E2;
212 | border: 1px solid #979797;
213 | width: 100%;
214 | height: 70px;
215 | margin-bottom: 20px;
216 | position: relative;
217 | padding-top: 13px;
218 |
219 | @include mobile {
220 | padding: 16px 5px 0px 8px;
221 | margin-bottom: 18px;
222 | }
223 |
224 | h1 {
225 | color: #fff;
226 | margin: 0;
227 | font-size: 30px;
228 |
229 | @include mobile {
230 | font-size: 20px;
231 | padding-top: 6px;
232 | display: none;
233 | }
234 | }
235 |
236 | .primary-navigation--actions {
237 | position: absolute;
238 | right: 0;
239 | top: 2px;
240 |
241 | @include mobile {
242 | position: static;
243 | }
244 |
245 | a {
246 | @include mobile {
247 | text-align: center;
248 | font-size: 12px;
249 | padding: 9px 7px;
250 | margin-left: 7px;
251 | color: #888;
252 |
253 | &.green {
254 | width: 47%;
255 | }
256 |
257 | &.blue {
258 | width: 47%;
259 | }
260 | }
261 |
262 | box-shadow: 2px 2px 2px rgba(0,0,0,0.2);
263 | display: inline-block;
264 | background: #F9F9F9;
265 | color: #918B8B;
266 |
267 | margin-left: 10px;
268 | padding: 9px 25px;
269 | text-transform: uppercase;
270 | text-decoration: none;
271 | font-weight: 600;
272 | font-size: 15px;
273 |
274 | &:hover {
275 | color: #ccc;
276 | }
277 | }
278 | }
279 | }
280 |
281 | .list-page-content {
282 | width: 100%;
283 | text-align: left;
284 | padding: 0;
285 | padding-bottom: 10px;
286 | padding-top: 10px;
287 | }
288 |
289 | .booking {
290 | padding: 20px;
291 | border-bottom: 1px solid #E8E6E6;
292 | position: relative;
293 |
294 | @include mobile {
295 | padding: 10px;
296 |
297 | &:last-child {
298 | padding-bottom: 0 !important;
299 | }
300 | }
301 |
302 |
303 | &:last-child {
304 | border-bottom: none;
305 | }
306 |
307 |
308 | .booking--info {
309 | h2 {
310 | font-size: 22px;
311 | margin: 0 0 5px 0;
312 |
313 | @include mobile {
314 | font-size: 18px;
315 | }
316 | }
317 |
318 | .booked-license-plate {
319 | color: rgba(0,0,0,0.8);
320 | }
321 |
322 | .booking--info--date-time {
323 | color: #666;
324 | margin: 5px 0;
325 |
326 | @include mobile {
327 | font-size: 15px;
328 | margin: 2px 0;
329 | }
330 |
331 | .time {
332 | // font-weight: bold;
333 | }
334 |
335 | .date {
336 | }
337 | }
338 |
339 | .booking--info--location {
340 | margin: 0;
341 | color: #666;
342 |
343 | @include mobile {
344 | margin-bottom: 10px;
345 | }
346 | }
347 |
348 | .booked-location--address {
349 | color: rgba(0,0,0,0.8);
350 |
351 | @include mobile {
352 | font-size: 15px;
353 | }
354 | }
355 | }
356 |
357 | .booking--actions {
358 | position: absolute;
359 | right: 40px;
360 | top: 30px;
361 |
362 | @include mobile {
363 | position: static;
364 | margin-bottom: 10px;
365 | }
366 |
367 | a {
368 | background: #eee;
369 | padding: 16px 40px;
370 | color: #888;
371 | text-decoration: none;
372 | display: inline-block;
373 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.13);
374 |
375 | @include desktop {
376 | width: 150px;
377 | text-align: center;
378 | }
379 |
380 | @include mobile {
381 | width: 100%;
382 | text-align: center;
383 | padding: 10px;
384 | font-size: 15px;
385 | }
386 | }
387 | }
388 | }
389 |
390 | .auth-flow-example {
391 | max-width: 100%;
392 | box-shadow: 0px 2px 15px 5px rgba(0, 0, 0, 0.09);
393 | margin-bottom: 20px;
394 | }
395 |
396 | .loading-overlay {
397 | position: fixed;
398 | top: 0;
399 | left: 0;
400 | width: 100%;
401 | height: 100%;
402 | background: rgba(255, 255, 255, 0.8);
403 | z-index: 1000;
404 | display: none;
405 |
406 | &.loading-overlay--is-visible {
407 | display: block;
408 | }
409 |
410 | .loading-overlay--animation {
411 | position: absolute;
412 | top: 30%;
413 | left: 50%;
414 | transform: translateX(-50%);
415 | }
416 |
417 | .sk-dot1 {
418 | background: rgba(8, 78, 93, 1);
419 | }
420 |
421 | .sk-dot2 {
422 | background: #2A4480;
423 | }
424 | }
425 |
426 | .form--actions {
427 | margin-top: 10px;
428 |
429 | @include desktop {
430 | margin-bottom: 15px;
431 | }
432 |
433 | @include mobile {
434 | button, a {
435 | display: block !important;
436 | margin-bottom: 8px;
437 | font-weight: 500;
438 | text-align: center;
439 | width: 100%;
440 | font-size: 16px;
441 | }
442 |
443 | button:last-child, a:last-child {
444 | margin-bottom: 0;
445 | }
446 | }
447 | }
448 |
449 | .login-or {
450 | margin-top: 10px!important;
451 | margin-bottom: 10px!important;
452 | color: #888;
453 | font-style: italic;
454 | font-weight: 300;
455 | text-align: center;
456 | }
457 |
458 | .faded-link {
459 | color: #888;
460 |
461 | &:hover {
462 | color: #ccc !important;
463 | }
464 |
465 | &:visited, &:active {
466 | color: #888;
467 | }
468 | }
469 |
470 | .page-subtitle {
471 | font-size: 24px;
472 | color: #B2B1B1;
473 | line-height: 27px;
474 | margin-top: 30px;
475 | margin-bottom: 15px;
476 | font-weight: 300;
477 | padding-left: 5px;
478 | font-weight: 500;
479 |
480 | @include mobile {
481 | margin-top: 10px;
482 | margin-bottom: 7px;
483 | font-size: 17px;
484 | padding-left: 10px;
485 | }
486 |
487 | .page-subtitle--link {
488 | color: #ccc;
489 | text-decoration: none;
490 | margin-right: 15px;
491 | letter-spacing: -1px;
492 |
493 | &.active {
494 | color: #B2B1B1;
495 | font-weight: 500;
496 | }
497 |
498 | &:hover, &.active:hover {
499 | color: #888;
500 | }
501 | }
502 | }
503 |
504 | .empty-list-notification {
505 | margin: 0 !important;
506 | text-align: center;
507 | padding: 30px;
508 | padding-bottom: 40px;
509 | width: 100%;
510 | max-width: 100% !important;
511 | padding-top: 50px;
512 | color: #B6B6B6;
513 | font-style: italic;
514 | }
515 |
516 | .booking-complete-button {
517 | background: #FFA500 !important;
518 | color: #FFFFFF !important;
519 | }
520 |
521 | .highlight-header {
522 | background: rgb(249, 249, 130);
523 | padding: 5px;
524 | width: 50%;
525 | margin: 20px auto;
526 | color: rgb(51, 51, 8);
527 | }
528 |
529 | .desktop-only {
530 | @include mobile {
531 | display: none !important;
532 | }
533 | }
534 |
535 | .mobile-only {
536 | @include desktop {
537 | display: none !important;
538 | }
539 | }
540 |
541 | .go-home-link {
542 | @include mobile {
543 | width: 97%;
544 | }
545 | }
546 |
547 | .in-progress-badge {
548 | font-size: 14px;
549 | vertical-align: middle;
550 | display: inline-block;
551 | background: yellow;
552 | margin-left: 10px;
553 | margin-bottom: 2px;
554 | }
555 |
556 | .link-with-options {
557 | position: relative;
558 | @include desktop {
559 | padding-right: 130px !important;
560 | }
561 | @include mobile {
562 | padding-right: 20px !important;
563 | }
564 | }
565 |
566 | $caret-size: 8px;
567 | $caret-size-mobile: 6px;
568 |
569 | .caret {
570 | width: 0;
571 | height: 0;
572 | display: inline-block;
573 | border-left: $caret-size solid transparent;
574 | border-right: $caret-size solid transparent;
575 | border-top: $caret-size solid #888;
576 |
577 | @include mobile {
578 | border-left: $caret-size-mobile solid transparent;
579 | border-right: $caret-size-mobile solid transparent;
580 | border-top: $caret-size-mobile solid #888;
581 | }
582 | }
583 |
584 | .link-options {
585 | width: 40px;
586 | height: 100%;
587 | position: absolute;
588 | right: 0;
589 | top: 0;
590 |
591 | &.grey {
592 | background: #E4E4E4;
593 |
594 | .caret {
595 | border-top: $caret-size solid #868686;
596 | }
597 |
598 | @include mobile {
599 | .caret {
600 | border-top: $caret-size-mobile solid #868686;
601 | }
602 | }
603 | }
604 |
605 | &.orange {
606 | background: #F0B758;
607 |
608 | .caret {
609 | border-top: $caret-size solid #A2690A;
610 | }
611 |
612 | @include mobile {
613 | .caret {
614 | border-top: $caret-size solid #A2690A;
615 | }
616 | }
617 | }
618 |
619 | .caret {
620 | position: absolute;
621 | top: 50%;
622 | left: 50%;
623 | transform: translate(-50%, -50%);
624 | }
625 | }
626 |
627 | .link-options-list {
628 | position: absolute;
629 | display: none;
630 | z-index: 999;
631 |
632 | @include desktop {
633 | // bottom: -53px;
634 | }
635 |
636 | @include mobile {
637 | // bottom: -20px;
638 | padding: 0px 10px;
639 | }
640 |
641 | left: 0;
642 | min-height: 20px;
643 | width: 100%;
644 | background: #fff;
645 |
646 | a:first-child {
647 | border-top: 1px solid #D6D6D6;
648 | }
649 |
650 | a {
651 | width: 100% !important;
652 | padding: 15px 10px !important;
653 | background: #F9F9F9 !important;
654 | }
655 |
656 | &.active {
657 | display: block;
658 | }
659 | }
660 |
661 | .address-input-wrapper {
662 | position: relative;
663 |
664 | input {
665 | padding-right: 35px !important;
666 | }
667 | }
668 |
669 | .get-current-location-button {
670 | position: absolute;
671 |
672 | &:hover {
673 | .ion-pinpoint {
674 | color: $blue;
675 | cursor: pointer;
676 | }
677 | }
678 |
679 | .ion-pinpoint {
680 | font-size: 22px;
681 | color: rgba(0,0,0,0.7);
682 | }
683 |
684 | @include mobile {
685 | right: 10px;
686 | top: 11px;
687 | }
688 |
689 | @include desktop {
690 | right: 15px;
691 | top: 17px;
692 | }
693 | }
694 |
--------------------------------------------------------------------------------