├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── comment_test.rb │ ├── relationship_test.rb │ ├── user_test.rb │ └── path_test.rb ├── controllers │ ├── .keep │ ├── api │ │ ├── paths_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── comments_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ └── relationships_controller_test.rb │ └── static_pages_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── comments.yml │ ├── relationships.yml │ ├── users.yml │ └── paths.yml ├── integration │ └── .keep └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── favicon.ico │ │ ├── global_bg.png │ │ ├── undoclear.png │ │ ├── stop_marker.png │ │ ├── default_avatar.png │ │ ├── friends_page.png │ │ ├── friends_search.png │ │ ├── frontpage-min.jpg │ │ ├── map_creation.png │ │ ├── middle_marker.png │ │ ├── path_show_page.png │ │ ├── sample_avatar.jpg │ │ └── start_marker.png │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── api │ │ │ ├── paths.coffee │ │ │ ├── users.coffee │ │ │ ├── comments.coffee │ │ │ ├── sessions.coffee │ │ │ └── relationships.coffee │ │ ├── static_pages.coffee │ │ ├── cable.js │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── homepage │ │ ├── home.scss │ │ ├── profile_box.scss │ │ ├── profile_tabs.scss │ │ ├── paths │ │ │ ├── map.scss │ │ │ ├── paths_index.scss │ │ │ ├── path_update_form.scss │ │ │ └── path_form.scss │ │ ├── activity │ │ │ └── activity.scss │ │ └── dashboard │ │ │ └── dashboard.scss │ │ ├── api │ │ ├── paths.scss │ │ ├── users.scss │ │ ├── comments.scss │ │ ├── sessions.scss │ │ └── relationships.scss │ │ ├── static_pages.scss │ │ ├── loading.scss │ │ ├── reset.scss │ │ ├── application.css │ │ ├── friends │ │ ├── friends_tab.scss │ │ ├── my_friends.scss │ │ └── friend_search.scss │ │ ├── session_form │ │ └── session_form.scss │ │ ├── masthead │ │ └── masthead.scss │ │ └── frontpage │ │ └── frontpage.scss ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── comment.rb │ ├── relationship.rb │ ├── path.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── sessions_controller.rb │ │ ├── comments_controller.rb │ │ ├── users_controller.rb │ │ ├── paths_controller.rb │ │ └── relationships_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ ├── paths │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── _path.json.jbuilder │ │ ├── users │ │ │ ├── show.json.jbuilder │ │ │ ├── _user.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── comments │ │ │ ├── show.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ └── _comment.json.jbuilder │ │ └── relationships │ │ │ └── index.json.jbuilder │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── api │ │ ├── paths_helper.rb │ │ ├── users_helper.rb │ │ ├── comments_helper.rb │ │ ├── sessions_helper.rb │ │ └── relationships_helper.rb │ ├── application_helper.rb │ └── static_pages_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── docs ├── wireframes │ ├── login.png │ ├── signup.png │ ├── homepage.png │ ├── dashboard.png │ ├── frontpage.png │ ├── activity_feed.png │ ├── create_routes.png │ ├── find_friends.png │ └── friends_page.png ├── api-endpoints.md ├── sample-state.md ├── component-hierarchy.md ├── README.MD └── schema.md ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config ├── boot.rb ├── environment.rb ├── cable.yml ├── spring.rb ├── initializers │ ├── session_store.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── new_framework_defaults.rb ├── application.rb ├── routes.rb ├── locales │ └── en.yml ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── puma.rb └── database.yml ├── config.ru ├── db ├── migrate │ ├── 20170428052157_add_default_to_paths_table.rb │ ├── 20170424015713_add_description_column_to_paths.rb │ ├── 20170428052457_add_description_default_to_paths_table.rb │ ├── 20170423044543_add_default_value_to_duration_to_paths_table.rb │ ├── 20170425032643_add_default_img_url_to_users.rb │ ├── 20170418174913_add_index_to_users.rb │ ├── 20170425033644_create_comments.rb │ ├── 20170418173523_create_users.rb │ ├── 20170427175516_create_relationships.rb │ └── 20170423005705_create_paths.rb └── schema.rb ├── Rakefile ├── frontend ├── actions │ ├── error_actions.js │ ├── session_actions.js │ ├── comment_actions.js │ ├── path_actions.js │ └── friend_actions.js ├── store │ └── store.js ├── components │ ├── app.jsx │ ├── home │ │ ├── paths │ │ │ ├── paths_main.jsx │ │ │ ├── paths_form │ │ │ │ ├── path_form_container.js │ │ │ │ ├── map_manager.js │ │ │ │ └── map.jsx │ │ │ ├── paths_index │ │ │ │ ├── path_index_container.js │ │ │ │ ├── path_index_item.jsx │ │ │ │ └── path_index.jsx │ │ │ └── path_show │ │ │ │ ├── comment_index_item.jsx │ │ │ │ ├── path_show_container.js │ │ │ │ ├── comments.jsx │ │ │ │ └── path_update_form.jsx │ │ ├── profile_container.jsx │ │ ├── dashboard │ │ │ ├── dashboard_container.js │ │ │ ├── pending_paths_index_item.jsx │ │ │ ├── complete_paths_index_item.jsx │ │ │ └── dashboard.jsx │ │ ├── activity_feed │ │ │ ├── activity_container.js │ │ │ ├── activity.jsx │ │ │ └── activity_index_item.jsx │ │ └── home.jsx │ ├── masthead │ │ ├── masthead_container.js │ │ └── masthead.jsx │ ├── friends │ │ ├── all_friends.jsx │ │ ├── friend_search_container.js │ │ ├── friends_index.jsx │ │ ├── all_friends_container.js │ │ ├── friends_tab.jsx │ │ ├── friend_requests_index.jsx │ │ └── friend_search.jsx │ ├── session_form │ │ └── session_form_container.js │ ├── frontpage │ │ └── frontpage.jsx │ └── root.jsx ├── util │ ├── session_api_util.js │ ├── comments_api_util.js │ ├── selector.js │ ├── paths_api_util.js │ ├── math_calculations.js │ └── friends_api_util.js ├── reducers │ ├── error_reducer.js │ ├── session_reducer.js │ ├── friend_requests_reducer.js │ ├── friend_search_reducer.js │ ├── friends_reducer.js │ ├── root_reducer.js │ ├── comments_reducer.js │ └── paths_reducer.js └── map_my_path.jsx ├── .gitignore ├── webpack.config.js ├── package.json └── Gemfile /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/helpers/api/paths_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::PathsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::CommentsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/relationships_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::RelationshipsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/paths/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/paths/path', path: @path 2 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/users/user', user: @user 2 | -------------------------------------------------------------------------------- /app/views/api/comments/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/comments/comment', comment: @comment 2 | -------------------------------------------------------------------------------- /docs/wireframes/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/login.png -------------------------------------------------------------------------------- /docs/wireframes/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/signup.png -------------------------------------------------------------------------------- /docs/wireframes/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/homepage.png -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/global_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/global_bg.png -------------------------------------------------------------------------------- /app/assets/images/undoclear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/undoclear.png -------------------------------------------------------------------------------- /docs/wireframes/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/dashboard.png -------------------------------------------------------------------------------- /docs/wireframes/frontpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/frontpage.png -------------------------------------------------------------------------------- /app/assets/images/stop_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/stop_marker.png -------------------------------------------------------------------------------- /docs/wireframes/activity_feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/activity_feed.png -------------------------------------------------------------------------------- /docs/wireframes/create_routes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/create_routes.png -------------------------------------------------------------------------------- /docs/wireframes/find_friends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/find_friends.png -------------------------------------------------------------------------------- /docs/wireframes/friends_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/docs/wireframes/friends_page.png -------------------------------------------------------------------------------- /app/assets/images/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/default_avatar.png -------------------------------------------------------------------------------- /app/assets/images/friends_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/friends_page.png -------------------------------------------------------------------------------- /app/assets/images/friends_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/friends_search.png -------------------------------------------------------------------------------- /app/assets/images/frontpage-min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/frontpage-min.jpg -------------------------------------------------------------------------------- /app/assets/images/map_creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/map_creation.png -------------------------------------------------------------------------------- /app/assets/images/middle_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/middle_marker.png -------------------------------------------------------------------------------- /app/assets/images/path_show_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/path_show_page.png -------------------------------------------------------------------------------- /app/assets/images/sample_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/sample_avatar.jpg -------------------------------------------------------------------------------- /app/assets/images/start_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulow/MapMyPath/HEAD/app/assets/images/start_marker.png -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/views/api/paths/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @paths.each do |path| 2 | json.set! path.id do 3 | json.partial! 'api/paths/path', path: path 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/views/api/relationships/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @friends.each do |friend| 2 | json.set! friend.id do 3 | json.partial! 'api/users/user', user: friend 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | config/application.yml 7 | ).each { |path| Spring.watch(path) } 8 | -------------------------------------------------------------------------------- /app/views/api/comments/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @comments.each do |comment| 2 | json.set! comment.id do 3 | json.partial! 'api/comments/comment', comment: comment 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_MapMyPath_session' 4 | -------------------------------------------------------------------------------- /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/20170428052157_add_default_to_paths_table.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultToPathsTable < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :paths, :done_date, '' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170424015713_add_description_column_to_paths.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionColumnToPaths < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :paths, :description, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/home.scss: -------------------------------------------------------------------------------- 1 | .body { 2 | background: image-url("global_bg.png"); 3 | } 4 | .main-content-container { 5 | margin: 0 auto; 6 | max-width: 940px; 7 | padding-bottom: 100px; 8 | } 9 | -------------------------------------------------------------------------------- /test/controllers/api/paths_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::PathsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/comments_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::CommentsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SessionsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/paths.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Paths controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170428052457_add_description_default_to_paths_table.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionDefaultToPathsTable < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :paths, :description, '' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/api/relationships_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::RelationshipsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/comments.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/comments controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/sessions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/Sessions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :first_name, :last_name, :img_url, :created_at 2 | 3 | # json.paths user.paths do |path| 4 | # json.extract! path, :name, :polyline, :distance, :created_at 5 | # end 6 | -------------------------------------------------------------------------------- /db/migrate/20170423044543_add_default_value_to_duration_to_paths_table.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultValueToDurationToPathsTable < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :paths, :duration, 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/relationships.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/relationships controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/paths.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/comments.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/sessions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/static_pages.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/relationships.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170425032643_add_default_img_url_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultImgUrlToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :users, :img_url, 'https://s3.us-east-2.amazonaws.com/mapmyrun-dev/default_avatar.png' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/comments/_comment.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! comment, :id, :author_id, :path_id, :body, :created_at 2 | 3 | json.set! :author do 4 | json.id comment.author.id 5 | json.name "#{comment.author.first_name} #{comment.author.last_name}" 6 | json.img_url comment.author.img_url 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /frontend/actions/error_actions.js: -------------------------------------------------------------------------------- 1 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 2 | export const ADD_ERRORS = "ADD_ERRORS"; 3 | 4 | export const clearErrors = () => ({ 5 | type: CLEAR_ERRORS 6 | }); 7 | 8 | export const addErrors = (errors) => ({ 9 | type: ADD_ERRORS, 10 | errors 11 | }); 12 | -------------------------------------------------------------------------------- /db/migrate/20170418174913_add_index_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_index :users, :email, unique: true 4 | add_index :users, :session_token, unique: true 5 | add_index :users, :first_name 6 | add_index :users, :last_name 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20170425033644_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :comments do |t| 4 | t.integer :author_id, null: false 5 | t.integer :path_id, null: false 6 | t.text :body, null: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/loading.scss: -------------------------------------------------------------------------------- 1 | #loading { 2 | border: 16px solid #f3f3f3; 3 | border-top: 16px solid #3498db; 4 | border-radius: 50%; 5 | width: 65px; 6 | height: 65px; 7 | animation: spin 1.5s linear infinite; 8 | } 9 | 10 | @keyframes spin { 11 | 0% { transform: rotate(0deg); } 12 | 100% { transform: rotate(360deg); } 13 | } 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | import rootReducer from '../reducers/root_reducer'; 5 | 6 | const configureStore = (preloadedState = {}) => ( 7 | createStore( 8 | rootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk) 11 | ) 12 | ); 13 | 14 | export default configureStore; 15 | // logger 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MastheadContainer from './masthead/masthead_container'; 3 | import { IntlProvider } from 'react-intl'; 4 | 5 | const App = ({ children, location }) => { 6 | return ( 7 | 8 |
9 | 10 | {children} 11 |
12 |
13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /db/migrate/20170418173523_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :email, null: false 5 | t.string :first_name, null: false 6 | t.string :last_name, null: false 7 | t.string :password_digest, null: false 8 | t.string :session_token, null: false 9 | t.string :img_url 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signUp = (user) => ( 2 | $.ajax({ 3 | method: 'POST', 4 | url: 'api/users', 5 | data: { user } 6 | }) 7 | ); 8 | 9 | export const login = (user) => ( 10 | $.ajax({ 11 | method: 'POST', 12 | url: 'api/session', 13 | data: { user } 14 | }) 15 | ); 16 | 17 | export const signOut = () => ( 18 | $.ajax({ 19 | method: 'DELETE', 20 | url: 'api/session' 21 | }) 22 | ); 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /db/migrate/20170427175516_create_relationships.rb: -------------------------------------------------------------------------------- 1 | class CreateRelationships < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :relationships do |t| 4 | t.integer :user_one_id, null: false 5 | t.integer :user_two_id, null: false 6 | t.integer :status, null: false 7 | t.integer :action_user_id, null: false 8 | 9 | t.timestamps 10 | end 11 | 12 | add_index :relationships, [:user_one_id, :user_two_id] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /frontend/reducers/error_reducer.js: -------------------------------------------------------------------------------- 1 | import { CLEAR_ERRORS, ADD_ERRORS } from '../actions/error_actions'; 2 | 3 | const defaultErrors = {}; 4 | 5 | const ErrorReducer = (state = defaultErrors, action) => { 6 | Object.freeze(state); 7 | switch(action.type) { 8 | case ADD_ERRORS: 9 | return Object.assign({}, action.errors.responseJSON); 10 | case CLEAR_ERRORS: 11 | return {}; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default ErrorReducer; 18 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions.js'; 2 | 3 | const _nullUser = { 4 | currentUser: null 5 | }; 6 | 7 | const SessionReducer = (state = _nullUser, action) => { 8 | Object.freeze(state); 9 | switch(action.type) { 10 | case RECEIVE_CURRENT_USER: 11 | const currentUser = action.currentUser; 12 | return Object.assign({}, { currentUser }); 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export default SessionReducer; 19 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PathFormContainer from './paths_form/path_form_container'; 3 | import PathIndex from './paths_index/path_index_container'; 4 | 5 | class PathsMain extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render () { 11 | return ( 12 |
13 | {this.props.children} 14 |
15 | ); 16 | } 17 | } 18 | 19 | // 20 | export default PathsMain; 21 | -------------------------------------------------------------------------------- /test/models/comment_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # path_id :integer not null 8 | # body :text not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class CommentTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_form/path_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createPath } from '../../../../actions/path_actions'; 3 | 4 | import PathForm from './path_form'; 5 | 6 | const mapStateToProps = ({ errors }) => { 7 | return ({ 8 | errors 9 | }); 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return { 14 | createPath: path => dispatch(createPath(path)) 15 | }; 16 | }; 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(PathForm); 22 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module MapMyPath 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/api/paths/_path.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! path, :id, :name, :polyline, :distance, :start_address, :end_address, :description, :duration, :done, :done_date, :created_at, :updated_at 2 | 3 | json.set! :user do 4 | json.id path.user.id 5 | json.name "#{path.user.first_name} #{path.user.last_name}" 6 | json.img_url path.user.img_url 7 | end 8 | 9 | # json.set! :comments do 10 | # json.array! path.comments do |comment| 11 | # json.id comment.id 12 | # json.author comment.author 13 | # json.body comment.body 14 | # end 15 | # end 16 | -------------------------------------------------------------------------------- /frontend/components/masthead/masthead_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { signOut } from '../../actions/session_actions'; 3 | import Masthead from './masthead'; 4 | 5 | const mapStateToProps = ({ session }, { location }) => { 6 | return ({ 7 | currentUser: session.currentUser, 8 | path: location.pathname 9 | }); 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => ({ 13 | signOut: () => dispatch(signOut()) 14 | }); 15 | 16 | export default connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(Masthead); 20 | -------------------------------------------------------------------------------- /frontend/util/comments_api_util.js: -------------------------------------------------------------------------------- 1 | export const getAllComments = (pathId) => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: `api/paths/${pathId}/comments` 5 | }); 6 | }; 7 | 8 | export const createComment = (comment) => { 9 | return $.ajax({ 10 | method: 'POST', 11 | url: `api/paths/${comment.path_id}/comments`, 12 | data: { comment } 13 | }); 14 | }; 15 | 16 | export const deleteComment = (comment) => { 17 | return $.ajax({ 18 | method: 'DELETE', 19 | url: `api/paths/${comment.path_id}/comments/${comment.id}`, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20170423005705_create_paths.rb: -------------------------------------------------------------------------------- 1 | class CreatePaths < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :paths do |t| 4 | t.string :name, null: false 5 | t.text :polyline, null: false 6 | t.float :distance, null: false 7 | t.string :start_address, null: false 8 | t.string :end_address, null: false 9 | t.integer :duration 10 | t.boolean :done, default: false 11 | t.date :done_date 12 | t.integer :user_id, null: false 13 | 14 | t.timestamps 15 | end 16 | 17 | add_index :paths, :user_id 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/reducers/friend_requests_reducer.js: -------------------------------------------------------------------------------- 1 | import { REMOVE_REQUEST, RECEIVE_ALL_REQUESTS } from '../actions/friend_actions'; 2 | 3 | const friendRequestReducer = (oldState = {}, action) => { 4 | Object.freeze(oldState); 5 | switch(action.type) { 6 | case RECEIVE_ALL_REQUESTS: 7 | return Object.assign({}, action.friends); 8 | case REMOVE_REQUEST: 9 | let newState = Object.assign({}, oldState); 10 | delete newState[action.friend.id]; 11 | return newState; 12 | default: 13 | return oldState; 14 | } 15 | }; 16 | 17 | export default friendRequestReducer; 18 | -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @friends.each do |friend| 2 | json.set! friend.id do 3 | json.partial! 'api/users/user', user: friend 4 | 5 | json.paths friend.paths do |path| 6 | json.extract! path, :name, :polyline, :distance, :updated_at, :id, :done 7 | json.user do 8 | json.name "#{friend.first_name} #{friend.last_name}" 9 | json.id friend.id 10 | json.img_url friend.img_url 11 | end 12 | end 13 | end 14 | end 15 | 16 | # json.array! friend.paths.each do |path| 17 | # json.extract! path, :id, :polyline, :distance, :updated_at 18 | # end 19 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | root "static_pages#root" 4 | 5 | namespace :api, defaults: { format: :json } do 6 | resources :users, only: [:create, :update, :index] 7 | resource :session, only: [:create, :destroy] 8 | resources :paths, only: [:create, :update, :destroy, :index, :show] do 9 | resources :comments, only: [:index, :create, :destroy] 10 | end 11 | resources :relationships, only: [:index, :update, :create, :destroy, :show] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /frontend/reducers/friend_search_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ALL_POTENTIAL_FRIENDS, REMOVE_FROM_SEARCH } from '../actions/friend_actions'; 2 | 3 | const friendSearchReducer = (oldState = {}, action) => { 4 | Object.freeze(oldState); 5 | switch(action.type) { 6 | case RECEIVE_ALL_POTENTIAL_FRIENDS: 7 | return Object.assign({}, action.friends); 8 | case REMOVE_FROM_SEARCH: 9 | let newState = Object.assign({}, oldState); 10 | delete newState[action.friend.id]; 11 | return newState; 12 | default: 13 | return oldState; 14 | } 15 | }; 16 | 17 | export default friendSearchReducer; 18 | -------------------------------------------------------------------------------- /frontend/components/home/profile_container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const Profile = ({ currentUser }) => { 5 | if(currentUser) { 6 | return ( 7 |
8 | 9 | 10 |

Hello,

11 |

{currentUser.first_name + ' ' + currentUser.last_name}

12 | Friends 13 |
14 |
15 | ); 16 | } else { 17 | return
; 18 | } 19 | }; 20 | 21 | export default Profile; 22 | -------------------------------------------------------------------------------- /test/models/relationship_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: relationships 4 | # 5 | # id :integer not null, primary key 6 | # user_one_id :integer not null 7 | # user_two_id :integer not null 8 | # status :integer not null 9 | # action_user_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | require 'test_helper' 15 | 16 | class RelationshipTest < ActiveSupport::TestCase 17 | # test "the truth" do 18 | # assert true 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # path_id :integer not null 8 | # body :text not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Comment < ApplicationRecord 14 | validates :author, :path, :body, presence: true 15 | 16 | belongs_to :author, 17 | class_name: 'User', 18 | primary_key: :id, 19 | foreign_key: :author_id 20 | 21 | belongs_to :path 22 | 23 | end 24 | -------------------------------------------------------------------------------- /frontend/map_my_path.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import configureStore from './store/store'; 4 | import Root from './components/root'; 5 | import Modal from 'react-modal'; 6 | 7 | 8 | document.addEventListener("DOMContentLoaded", () => { 9 | let store; 10 | if (window.currentUser) { 11 | const preloadedState = { session: { currentUser: window.currentUser } }; 12 | store = configureStore(preloadedState); 13 | } else { 14 | store = configureStore(); 15 | } 16 | 17 | Modal.setAppElement(document.body); 18 | const root = document.getElementById('root'); 19 | ReactDOM.render(, root); 20 | }); 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | node_modules/ 20 | bundle.js 21 | bundle.js.map 22 | .DS_Store 23 | npm-debug.log 24 | 25 | # Ignore application configuration 26 | /config/application.yml 27 | -------------------------------------------------------------------------------- /frontend/util/selector.js: -------------------------------------------------------------------------------- 1 | export const completePaths = ( paths ) => { 2 | let completedPaths = []; 3 | for (const pathId in paths) { 4 | if(paths[pathId].done === true) { 5 | completedPaths.push(paths[pathId]); 6 | } 7 | } 8 | return completedPaths.sort(function(a, b) { return new Date(b.updated_at) - new Date(a.updated_at); }); 9 | }; 10 | 11 | export const pendingPaths = ( paths ) => { 12 | let incompletedPaths = []; 13 | for (const pathId in paths) { 14 | if(paths[pathId].done === false) { 15 | incompletedPaths.push(paths[pathId]); 16 | } 17 | } 18 | return incompletedPaths.sort(function(a, b) { return new Date(b.updated_at) - new Date(a.updated_at); }); 19 | }; 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, header, nav, h1, h2, h3, h4, h5, a, 2 | ul, li, strong, main, button, 3 | section, img, div, p, form, 4 | fieldset, label, input, textarea, 5 | span, article, footer, time, small, dl, dt, dd { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | font-family: 'Roboto','Open Sans', sans-serif; 11 | color: inherit; 12 | text-align: inherit; 13 | text-decoration: inherit; 14 | vertical-align: inherit; 15 | box-sizing: inherit; 16 | background: transparent; 17 | -webkit-font-smoothing: antialiased; 18 | list-style: none; 19 | } 20 | 21 | input:focus, 22 | select:focus, 23 | textarea:focus, 24 | button:focus { 25 | outline: none; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_index/path_index_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import PathIndex from './path_index'; 3 | 4 | import { requestAllPaths, deletePath } from '../../../../actions/path_actions'; 5 | 6 | const mapStateToProps = ({ paths }) => { 7 | return { 8 | paths: Object.keys(paths).map(id => paths[id]) 9 | .sort(function(a, b) { return b.id - a.id; }) 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | return { 15 | requestAllPaths: () => dispatch(requestAllPaths('index')), 16 | deletePath: (id) => dispatch(deletePath(id)) 17 | }; 18 | }; 19 | 20 | export default connect( 21 | mapStateToProps, 22 | mapDispatchToProps 23 | )(PathIndex); 24 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/models/relationship.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: relationships 4 | # 5 | # id :integer not null, primary key 6 | # user_one_id :integer not null 7 | # user_two_id :integer not null 8 | # status :integer not null 9 | # action_user_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | class Relationship < ApplicationRecord 15 | validates :user_one_id, :user_two_id, :status, :action_user_id, presence: true 16 | validates :user_one_id, uniqueness: { scope: :user_two_id } 17 | 18 | # status: 0 pending friend request 19 | # status: 1 confirmed friend 20 | 21 | 22 | end 23 | -------------------------------------------------------------------------------- /frontend/reducers/friends_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ALL_FRIENDS, REMOVE_FRIEND, RECEIVE_NEW_FRIEND } from '../actions/friend_actions'; 2 | 3 | 4 | const FriendsReducer = (oldState = {}, action) => { 5 | Object.freeze(oldState); 6 | switch(action.type) { 7 | case RECEIVE_ALL_FRIENDS: 8 | return Object.assign({}, action.friends); 9 | case RECEIVE_NEW_FRIEND: 10 | return Object.assign( 11 | {}, 12 | oldState, 13 | { [action.friend.id]: action.friend} 14 | ); 15 | case REMOVE_FRIEND: 16 | let newState = Object.assign({}, oldState); 17 | delete newState[action.id]; 18 | return newState; 19 | default: 20 | return oldState; 21 | } 22 | }; 23 | 24 | export default FriendsReducer; 25 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials( 5 | params[:user][:email], 6 | params[:user][:password] 7 | ) 8 | if @user 9 | log_in(@user) 10 | render 'api/users/show' 11 | else 12 | render( 13 | json: { login: ["Incorrect email or password. Please try again"] }, 14 | status: 401 15 | ) 16 | end 17 | end 18 | 19 | def destroy 20 | @user = current_user 21 | 22 | if @user 23 | sign_out 24 | render 'api/users/show' 25 | else 26 | render( 27 | json: { signout: ["No user logged in"] }, 28 | status: 404 29 | ) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # email :string not null 7 | # first_name :string not null 8 | # last_name :string not null 9 | # password_digest :string not null 10 | # session_token :string not null 11 | # img_url :string default("https://s3.us-east-2.amazonaws.com/mapmyrun-dev/default_avatar.png") 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # 15 | 16 | require 'test_helper' 17 | 18 | class UserTest < ActiveSupport::TestCase 19 | # test "the truth" do 20 | # assert true 21 | # end 22 | end 23 | -------------------------------------------------------------------------------- /test/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # path_id :integer not null 8 | # body :text not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | # This model initially had no columns defined. If you add columns to the 16 | # model remove the '{}' from the fixture names and add the columns immediately 17 | # below each fixture, per the syntax in the comments below 18 | # 19 | one: {} 20 | # column: value 21 | # 22 | two: {} 23 | # column: value 24 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import SessionReducer from './session_reducer'; 4 | import ErrorReducer from './error_reducer'; 5 | import PathsReducer from './paths_reducer'; 6 | import CommentsReducer from './comments_reducer'; 7 | import FriendsReducer from './friends_reducer'; 8 | import FriendRequestReducer from './friend_requests_reducer'; 9 | import FriendSearchReducer from './friend_search_reducer.js'; 10 | 11 | const rootReducer = combineReducers({ 12 | session: SessionReducer, 13 | errors: ErrorReducer, 14 | paths: PathsReducer, 15 | comments: CommentsReducer, 16 | friends: FriendsReducer, 17 | friendRequests: FriendRequestReducer, 18 | friendSearches: FriendSearchReducer 19 | }); 20 | 21 | export default rootReducer; 22 | -------------------------------------------------------------------------------- /frontend/reducers/comments_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ALL_COMMENTS, 3 | RECEIVE_NEW_COMMENT, 4 | REMOVE_COMMENT 5 | } from '../actions/comment_actions'; 6 | 7 | 8 | const CommentsReducer = (oldState = {}, action) => { 9 | Object.freeze(oldState); 10 | switch(action.type) { 11 | case RECEIVE_ALL_COMMENTS: 12 | return Object.assign({}, action.comments); 13 | case RECEIVE_NEW_COMMENT: 14 | return Object.assign( 15 | {}, 16 | oldState, 17 | { [action.comment.id]: action.comment } 18 | ); 19 | case REMOVE_COMMENT: 20 | let newState = Object.assign({}, oldState); 21 | delete newState[action.id]; 22 | return newState; 23 | default: 24 | return oldState; 25 | } 26 | }; 27 | 28 | export default CommentsReducer; 29 | -------------------------------------------------------------------------------- /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. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /app/controllers/api/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CommentsController < ApplicationController 2 | before_action :require_logged_in! 3 | 4 | def index 5 | # double check 6 | @comments = Comment.where('path_id = ?', params[:path_id]).includes(:author) 7 | end 8 | 9 | def create 10 | @comment = current_user.comments.new(comment_params) 11 | @comment.author_id = current_user.id 12 | 13 | if @comment.save 14 | render :show 15 | else 16 | render json: @comment.errors.messages, status: 422 17 | end 18 | end 19 | 20 | def destroy 21 | @comment = Comment.find(params[:id]) 22 | @comment.destroy 23 | render json: @comment 24 | end 25 | 26 | private 27 | 28 | def comment_params 29 | params.require(:comment).permit(:body, :path_id) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 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 other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | helper_method :current_user, :logged_in? 5 | 6 | private 7 | 8 | def current_user 9 | return nil unless session[:session_token] 10 | @current_user ||= User.find_by_session_token(session[:session_token]) 11 | end 12 | 13 | def logged_in? 14 | !!current_user 15 | end 16 | 17 | def log_in(user) 18 | @current_user = user 19 | session[:session_token] = user.reset_session_token! 20 | end 21 | 22 | def sign_out 23 | current_user.try(:reset_session_token!) 24 | session[:session_token] = nil 25 | end 26 | 27 | def require_logged_in! 28 | render json: { base: ['invalid credentials'] }, status: 401 if !current_user 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /frontend/components/home/dashboard/dashboard_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Dashboard from './dashboard'; 3 | 4 | import { requestAllPaths, clearPaths } from '../../../actions/path_actions'; 5 | 6 | import { completePaths, pendingPaths } from '../../../util/selector.js'; 7 | 8 | const mapStateToProps = ({ paths, session }) => { 9 | return { 10 | completePaths: completePaths(paths), 11 | pendingPaths: pendingPaths(paths), 12 | currentUser: session.currentUser 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | requestAllPaths: () => dispatch(requestAllPaths('index')), 19 | clearPaths: () => dispatch(clearPaths()) 20 | }; 21 | }; 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(Dashboard); 27 | -------------------------------------------------------------------------------- /test/models/path_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: paths 4 | # 5 | # id :integer not null, primary key 6 | # name :string not null 7 | # polyline :text not null 8 | # distance :float not null 9 | # start_address :string not null 10 | # end_address :string not null 11 | # duration :integer default("0") 12 | # done :boolean default("false") 13 | # done_date :date 14 | # user_id :integer not null 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # description :text 18 | # 19 | 20 | require 'test_helper' 21 | 22 | class PathTest < ActiveSupport::TestCase 23 | # test "the truth" do 24 | # assert true 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /test/fixtures/relationships.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: relationships 4 | # 5 | # id :integer not null, primary key 6 | # user_one_id :integer not null 7 | # user_two_id :integer not null 8 | # status :integer not null 9 | # action_user_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 15 | 16 | # This model initially had no columns defined. If you add columns to the 17 | # model remove the '{}' from the fixture names and add the columns immediately 18 | # below each fixture, per the syntax in the comments below 19 | # 20 | one: {} 21 | # column: value 22 | # 23 | two: {} 24 | # column: value 25 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API endpoints 2 | 3 | ## HTML API 4 | 5 | ### RoutesContainer- `GET /` - loads React web app 6 | 7 | ## JSON API 8 | 9 | ### Users 10 | 11 | - `POST /api/users` 12 | - `PATCH /api/users` 13 | 14 | ### Session 15 | 16 | - `POST /api/session` 17 | - `DELETE /api/session` 18 | 19 | ### Routes 20 | 21 | - `GET /api/routes` 22 | - `POST /api/routes` 23 | - `GET /api/routes/:id` 24 | - `PATCH /api/routes/:id` 25 | - `DELETE /api/routes/:id` 26 | 27 | ### Friends 28 | 29 | - `GET /api/friends` 30 | - `DELETE /api/friends/:id` 31 | 32 | ### Friend Requests 33 | 34 | - `POST /api/friend_requests` 35 | - `DELETE /api/friend_requests/:id` 36 | 37 | ### Status 38 | 39 | - `GET /api/status` 40 | - `POST /api/status` 41 | - `DELETE /api/status/:id` 42 | 43 | ### Comments 44 | 45 | - `GET /api/comments` 46 | - `POST /api/comments` 47 | - `DELETE /api/comments/:id` 48 | -------------------------------------------------------------------------------- /frontend/components/friends/all_friends.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FriendsIndex from './friends_index'; 3 | import FriendRequestsIndex from './friend_requests_index'; 4 | 5 | class AllFriends extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount() { 11 | this.props.requestAllRequests(); 12 | this.props.requestAllFriends(); 13 | } 14 | 15 | render () { 16 | return ( 17 |
18 | 21 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default AllFriends; 31 | -------------------------------------------------------------------------------- /frontend/components/friends/friend_search_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import FriendSearch from './friend_search'; 3 | 4 | import { 5 | addRequest, 6 | requestAllPotentialFriends 7 | } from '../../actions/friend_actions'; 8 | 9 | const mapStateToProps = ({ session, friendSearches }) => { 10 | return { 11 | currentUser: session.currentUser, 12 | potentialFriends: Object.keys(friendSearches).map(id => friendSearches[id]) 13 | .sort(function(a, b) { return a.first_name - b.first_name; }), 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | addRequest: (id) => dispatch(addRequest(id)), 20 | requestAllPotentialFriends: (search) => 21 | dispatch(requestAllPotentialFriends(search)) 22 | }; 23 | }; 24 | 25 | export default connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(FriendSearch); 29 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /app/models/path.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: paths 4 | # 5 | # id :integer not null, primary key 6 | # name :string not null 7 | # polyline :text not null 8 | # distance :float not null 9 | # start_address :string not null 10 | # end_address :string not null 11 | # duration :integer default("0") 12 | # done :boolean default("false") 13 | # done_date :date 14 | # user_id :integer not null 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # description :text 18 | # 19 | 20 | class Path < ApplicationRecord 21 | validates :name, :polyline, :distance, :start_address, :end_address, :user_id, presence: true 22 | 23 | belongs_to :user 24 | has_many :comments, dependent: :destroy 25 | 26 | end 27 | -------------------------------------------------------------------------------- /frontend/components/friends/friends_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const FriendsIndex = ({ friends, deleteFriend }) => { 5 | const friendListItem = (friend) => { 6 | return ( 7 |
  • 8 | 9 |
    10 |

    { friend.first_name + " " + friend.last_name }

    11 | 12 |
    13 |
  • 14 | ); 15 | }; 16 | 17 | 18 | return ( 19 |
    20 |
    21 |

    Friends

    22 | Back to home page 23 |
    24 |
      25 | {friends.map(friend => friendListItem(friend))} 26 |
    27 |
    28 | ); 29 | }; 30 | 31 | export default FriendsIndex; 32 | -------------------------------------------------------------------------------- /frontend/components/home/activity_feed/activity_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Activity from './activity'; 3 | 4 | import { combinePathsAndFriendsActivities } from '../../../util/selector.js'; 5 | 6 | import { requestAllPaths, clearPaths } from '../../../actions/path_actions'; 7 | import { requestAllFriends } from '../../../actions/friend_actions'; 8 | 9 | const mapStateToProps = ({ session, paths }) => { 10 | return { 11 | currentUser: session.currentUser, 12 | paths: Object.keys(paths).map(id => paths[id]) 13 | .sort(function(a, b) { return b.id - a.id; }) 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | requestAllPaths: () => dispatch(requestAllPaths('activity')), 20 | clearPaths: () => dispatch(clearPaths()) 21 | }; 22 | }; 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(Activity); 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | context: __dirname, 6 | entry: './frontend/map_my_path.jsx', 7 | output: { 8 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 9 | filename: 'bundle.js' 10 | }, 11 | resolve: { 12 | extensions: ['.js', '.jsx', '*'] 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.jsx?$/, 18 | exclude: /(node_modules|bower_components)/, 19 | loader: 'babel-loader', 20 | query: { 21 | presets: ['react', 'es2015'] 22 | } 23 | } 24 | ] 25 | }, 26 | devtool: 'source-maps', 27 | // plugins: [ 28 | // new webpack.DefinePlugin({ 29 | // 'process.env': { 30 | // NODE_ENV: JSON.stringify('production') 31 | // } 32 | // }), 33 | // new webpack.optimize.UglifyJsPlugin() 34 | // ] 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/util/paths_api_util.js: -------------------------------------------------------------------------------- 1 | export const getAllPaths = (type) => ( 2 | $.ajax({ 3 | method: 'GET', 4 | url: 'api/paths', 5 | data: { type } 6 | }) 7 | ); 8 | 9 | export const getSinglePath = (id) => ( 10 | $.ajax({ 11 | method: 'GET', 12 | url: `api/paths/${id}` 13 | }) 14 | ); 15 | 16 | export const createPath = (path) => ( 17 | $.ajax({ 18 | method: 'POST', 19 | url: 'api/paths', 20 | data: { path } 21 | }) 22 | ); 23 | 24 | export const updatePath = (path) => ( 25 | $.ajax({ 26 | method: 'PATCH', 27 | url: `api/paths/${path.id}`, 28 | data: { path } 29 | }) 30 | ); 31 | 32 | export const deletePath = (id) => ( 33 | $.ajax({ 34 | method: 'DELETE', 35 | url: `api/paths/${id}` 36 | }) 37 | ); 38 | 39 | // path = { 40 | // name: 'Sample', 41 | // polyline: 'Test', 42 | // distance: '5', 43 | // start_address: 'here', 44 | // end_address: 'there' 45 | // } 46 | -------------------------------------------------------------------------------- /frontend/components/home/dashboard/pending_paths_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const PendingPathsItem = ({ paths }) => { 5 | return ( 6 | 26 | ); 27 | }; 28 | 29 | export default PendingPathsItem; 30 | -------------------------------------------------------------------------------- /frontend/reducers/paths_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ALL_PATHS, 3 | RECEIVE_NEW_PATH, 4 | REMOVE_PATH, 5 | RECEIVE_SINGLE_PATH, 6 | CLEAR_PATHS 7 | } from '../actions/path_actions'; 8 | 9 | 10 | const PathsReducer = (oldState = {}, action) => { 11 | Object.freeze(oldState); 12 | switch(action.type) { 13 | case RECEIVE_ALL_PATHS: 14 | return Object.assign({}, action.paths); 15 | case RECEIVE_NEW_PATH: 16 | return Object.assign( 17 | {}, 18 | oldState, 19 | { [action.path.id]: action.path } 20 | ); 21 | case RECEIVE_SINGLE_PATH: 22 | return Object.assign({}, { [action.path.id]: action.path }); 23 | case REMOVE_PATH: 24 | let newState = Object.assign({}, oldState); 25 | delete newState[action.id]; 26 | return newState; 27 | case CLEAR_PATHS: 28 | return {}; 29 | default: 30 | return oldState; 31 | } 32 | }; 33 | 34 | export default PathsReducer; 35 | -------------------------------------------------------------------------------- /frontend/components/session_form/session_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { login, signOut, signUp } from '../../actions/session_actions'; 3 | import { clearErrors } from '../../actions/error_actions'; 4 | import SessionForm from './session_form'; 5 | 6 | const mapStateToProps = ({ session, errors }) => { 7 | return ({ 8 | signedIn: Boolean(session.currentUser), 9 | errors 10 | }); 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch, { location }) => { 14 | const formType = location.pathname.slice(1); 15 | let processForm = signUp; 16 | if (formType === 'login') { 17 | processForm = login; 18 | } 19 | return { 20 | processForm: user => dispatch(processForm(user)), 21 | loginGuest: guest => dispatch(login(guest)), 22 | clearErrors: () => dispatch(clearErrors()), 23 | formType 24 | }; 25 | }; 26 | 27 | export default connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(SessionForm); 31 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # email :string not null 7 | # first_name :string not null 8 | # last_name :string not null 9 | # password_digest :string not null 10 | # session_token :string not null 11 | # img_url :string default("https://s3.us-east-2.amazonaws.com/mapmyrun-dev/default_avatar.png") 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # 15 | 16 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 17 | 18 | # This model initially had no columns defined. If you add columns to the 19 | # model remove the '{}' from the fixture names and add the columns immediately 20 | # below each fixture, per the syntax in the comments below 21 | # 22 | one: {} 23 | # column: value 24 | # 25 | two: {} 26 | # column: value 27 | -------------------------------------------------------------------------------- /frontend/components/home/activity_feed/activity.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Profile from '../profile_container'; 3 | 4 | import ActivityIndexItem from './activity_index_item'; 5 | 6 | class Activity extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount () { 12 | this.props.requestAllPaths(); 13 | } 14 | 15 | componentWillUnmount () { 16 | this.props.clearPaths(); 17 | } 18 | 19 | render () { 20 | return ( 21 |
    22 |
    23 |
    24 |

    25 | recent activities 26 |

    27 |
    28 | 32 |
    33 | 34 |
    35 | ); 36 | } 37 | } 38 | 39 | export default Activity; 40 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/profile_box.scss: -------------------------------------------------------------------------------- 1 | .profile-box { 2 | display: flex; 3 | border: 1px solid #cdcdcd; 4 | background: #f8f8f8; 5 | border-radius: 3px; 6 | width: 230px; 7 | padding: 8px; 8 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 9 | margin: 15px 0 15px 30px; 10 | text-transform: uppercase; 11 | height: 100%; 12 | 13 | p:first-child { 14 | font-size: 12px; 15 | margin-bottom: 10px; 16 | letter-spacing: .8px; 17 | } 18 | 19 | .name { 20 | padding-bottom: 10px; 21 | border-bottom: 1px solid #cdcdcd; 22 | letter-spacing: .5px; 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | width: 120px; 26 | } 27 | 28 | a { 29 | font-size: 13px; 30 | display: block; 31 | color:#2f98d9; 32 | margin-top: 10px; 33 | align-self: flex-end; 34 | } 35 | 36 | img { 37 | height: 100px; 38 | width: 100px; 39 | border: 1px solid #cdcdcd; 40 | background: #fff; 41 | } 42 | 43 | span { 44 | margin-left: 5px; 45 | padding: 5px; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/util/math_calculations.js: -------------------------------------------------------------------------------- 1 | export const convertSecondsToTime = (secNum) => { 2 | let hours = Math.floor(secNum / 3600); 3 | let minutes = Math.floor((secNum - (hours * 3600)) / 60); 4 | let seconds = secNum - (hours * 3600) - (minutes * 60); 5 | 6 | if (hours < 10) {hours = "0"+hours;} 7 | if (minutes < 10) {minutes = "0"+minutes;} 8 | if (seconds < 10) {seconds = "0"+seconds;} 9 | return (hours+':'+minutes+':'+seconds); 10 | }; 11 | 12 | export const convertTimeToSeconds = (hours, minutes, seconds) => { 13 | const hoursSeconds = hours * 3600; 14 | const minutesSeconds = minutes * 60; 15 | return (hoursSeconds + minutesSeconds + seconds); 16 | }; 17 | 18 | export const todaysDate = () => { 19 | const today = new Date(); 20 | const year = today.getFullYear(); 21 | let month = today.getMonth() + 1; 22 | let date = today.getDate(); 23 | if (date < 10) { 24 | date = '0' + date; 25 | } 26 | if(month < 10){ 27 | month = '0'+ month; 28 | } 29 | 30 | return (year + '-' + month + '-' + date); 31 | }; 32 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 782d6a54f6db812485cd79d59c06ffe2bec8e049511a3baf40e0aaaf98e8f730e69c44ede0f35f3a27c91a4387da30cae16cda64cf7f7ff1fdb5a9d03e1b301f 15 | 16 | test: 17 | secret_key_base: 6ad88cecf8526461bfe28b5b605ca479973844a10f1fe3b1d8478e6a733c1f037c58765dcf71a906743bc78b052d255c0b5419622d4c28ab086b9b7651de8170 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 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_index/path_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | const PathIndexItem = ({ path, deletePath, router }) => { 5 | const map = `https://maps.googleapis.com/maps/api/staticmap?size=80x80&path=weight:3%7Ccolor:red%7Cenc:${ path.polyline }&key=AIzaSyBygQhRnDSS9s1hu7jxsQMu3mwIU7Hd2N4`; const complete = (path.done) ? 'Yes' : 'No'; 6 | return ( 7 | 8 | 9 | {path.created_at.slice(0, 10)} 10 | {path.distance} mi 11 | {path.name} 12 | {path.start_address} 13 | {complete} 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default withRouter(PathIndexItem); 23 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | def index 3 | @friends = current_user.friends 4 | render :index 5 | end 6 | 7 | def create 8 | @user = User.new(user_params) 9 | if @user.save 10 | log_in(@user) 11 | render :show 12 | else 13 | render( 14 | json: @user.errors.messages, 15 | status: 422 16 | # will render a key value pair, e.g. first_name => ["can't be blank"] 17 | ) 18 | end 19 | end 20 | 21 | def update 22 | @user = User.find_by(id: params[:id]) 23 | 24 | if @user.update(user_params) 25 | render :show 26 | else 27 | render( 28 | json: @user.errors.messages, 29 | status: 422 30 | # will render a key value pair, e.g. first_name => ["can't be blank"] 31 | ) 32 | end 33 | end 34 | 35 | private 36 | 37 | def user_params 38 | params.require(:user).permit( 39 | :first_name, 40 | :last_name, 41 | :email, 42 | :password, 43 | :img_url 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/fixtures/paths.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: paths 4 | # 5 | # id :integer not null, primary key 6 | # name :string not null 7 | # polyline :text not null 8 | # distance :float not null 9 | # start_address :string not null 10 | # end_address :string not null 11 | # duration :integer default("0") 12 | # done :boolean default("false") 13 | # done_date :date 14 | # user_id :integer not null 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # description :text 18 | # 19 | 20 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 21 | 22 | # This model initially had no columns defined. If you add columns to the 23 | # model remove the '{}' from the fixture names and add the columns immediately 24 | # below each fixture, per the syntax in the comments below 25 | # 26 | one: {} 27 | # column: value 28 | # 29 | two: {} 30 | # column: value 31 | -------------------------------------------------------------------------------- /frontend/util/friends_api_util.js: -------------------------------------------------------------------------------- 1 | export const getAllFriends = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: 'api/users' 5 | }); 6 | }; 7 | 8 | export const getPendingFriends = () => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: 'api/relationships' 12 | }); 13 | }; 14 | 15 | export const addFriend = (friend_id) => ( 16 | $.ajax({ 17 | method: 'PATCH', 18 | url: `api/relationships/${friend_id}`, 19 | }) 20 | ); 21 | 22 | export const deleteFriend = (friend_id) => ( 23 | $.ajax({ 24 | method: 'DELETE', 25 | url: `api/relationships/${friend_id}` 26 | }) 27 | ); 28 | 29 | export const addRequest = (friend_id) => ( 30 | $.ajax({ 31 | method: 'POST', 32 | url: `api/relationships/`, 33 | data: { friend_id } 34 | }) 35 | ); 36 | 37 | export const deleteRequest = (friend_id) => ( 38 | $.ajax({ 39 | method: 'DELETE', 40 | url: `api/relationships/${friend_id}` 41 | }) 42 | ); 43 | 44 | export const getPotentialFriends = (search) => ( 45 | $.ajax({ 46 | method: 'GET', 47 | url: 'api/relationships/search', 48 | data: { search } 49 | }) 50 | ); 51 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/profile_tabs.scss: -------------------------------------------------------------------------------- 1 | .profile-tabs-container { 2 | 3 | .profile-tabs { 4 | display: flex; 5 | padding: 35px 0 20px 0; 6 | color: #046AB2; 7 | 8 | a { 9 | font-size: 14px; 10 | text-transform: uppercase; 11 | padding: 12px 23px; 12 | width: 700px; 13 | text-align: center; 14 | background: #f6f6f6; 15 | border-top: 1px solid #d1d0cf; 16 | border-bottom: 1px solid #d1d0cf; 17 | border-right: 1px solid #d1d0cf; 18 | font-weight: 700; 19 | 20 | 21 | &:hover { 22 | color: #92BFDD; 23 | background: #fff; 24 | } 25 | 26 | &:first-child { 27 | border-left: 1px solid #dddddd; 28 | } 29 | } 30 | 31 | .selected { 32 | border-top: 3.5px solid #2BA5EF; 33 | color: #444444; 34 | background: #fff; 35 | border-bottom: 0; 36 | cursor: default; 37 | 38 | &:hover { 39 | color: #444444; 40 | background: #fff; 41 | } 42 | } 43 | 44 | .end-border { 45 | border-bottom: 1px solid #d1d0cf; 46 | width: 100%; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/components/home/paths/path_show/comment_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedRelative } from 'react-intl'; 3 | 4 | const CommentIndexItem = ({ comment, deleteComment, path, currentUser }) => { 5 | const deleteButton = () => { 6 | if (comment.author_id === currentUser.id || path.user.id === currentUser.id) { 7 | return ; 8 | } else { 9 | return ""; 10 | } 11 | }; 12 | return ( 13 |
  • 14 |
    15 | 16 |
    17 |
    18 | 19 |

    { comment.author.name }

    20 |
    21 | 22 | { deleteButton() } 23 |
    24 |
    25 | 26 |

    { comment.body }

    27 |
    28 |
    29 |
  • 30 | ); 31 | }; 32 | 33 | export default CommentIndexItem; 34 | -------------------------------------------------------------------------------- /frontend/components/friends/all_friends_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import AllFriends from './all_friends'; 3 | 4 | import { 5 | requestAllFriends, 6 | deleteFriend, 7 | deleteRequest, 8 | addFriend, 9 | requestAllRequests, 10 | } from '../../actions/friend_actions'; 11 | 12 | const mapStateToProps = ({ session, friends, friendRequests }) => { 13 | return { 14 | currentUser: session.currentUser, 15 | friends: Object.keys(friends).map(id => friends[id]) 16 | .sort(function(a, b) { return b.name - a.name; }), 17 | friendRequests: Object.keys(friendRequests).map(id => friendRequests[id]) 18 | .sort(function(a, b) { return b.name - a.name; }) 19 | }; 20 | }; 21 | 22 | const mapDispatchToProps = (dispatch) => { 23 | return { 24 | requestAllFriends: () => dispatch(requestAllFriends()), 25 | deleteFriend: (id) => dispatch(deleteFriend(id)), 26 | deleteRequest: (id) => dispatch(deleteRequest(id)), 27 | addFriend: (id) => dispatch(addFriend(id)), 28 | requestAllRequests: () => dispatch(requestAllRequests()) 29 | }; 30 | }; 31 | 32 | export default connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(AllFriends); 36 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 6 | 7 | # Enable per-form CSRF tokens. Previous versions had false. 8 | Rails.application.config.action_controller.per_form_csrf_tokens = true 9 | 10 | # Enable origin-checking CSRF mitigation. Previous versions had false. 11 | Rails.application.config.action_controller.forgery_protection_origin_check = true 12 | 13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 14 | # Previous versions had false. 15 | ActiveSupport.to_time_preserves_timezone = true 16 | 17 | # Require `belongs_to` associations by default. Previous versions had false. 18 | Rails.application.config.active_record.belongs_to_required_by_default = true 19 | 20 | # Do not halt callback chains when a callback returns false. Previous versions had true. 21 | ActiveSupport.halt_callback_chains_on_return_false = false 22 | 23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 25 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ```js 2 | { 3 | session: { 4 | currentUser: { 5 | id: 1, 6 | name: "Eugene", 7 | img_url: "url" 8 | } 9 | }, 10 | 11 | errors: { 12 | email: "cannot be blank", 13 | first_name: "cannot be blank", 14 | password: "is too short (minimum is 6 characters)" 15 | }, 16 | 17 | paths: { 18 | 1: { 19 | user_id: 1, 20 | name: "Sample path", 21 | polyline: "", 22 | distance: "5 miles", 23 | done: false, 24 | map_url: "", 25 | duration: null 26 | }, 27 | 2: { 28 | user_id: 2, 29 | name: "Sample path 2", 30 | polyline: "", 31 | distance: "5 miles", 32 | done: true, 33 | map_url: "", 34 | duration: 60 35 | } 36 | }, 37 | 38 | friends: { 39 | 2: { 40 | id: 2, 41 | name: "Jennifer", 42 | img_url: "url" 43 | }, 44 | 3: { 45 | id: 3, 46 | name: "Kevin", 47 | img_url: "url" 48 | }, 49 | 4: { 50 | id: 4, 51 | name: "Josh", 52 | img_url: "url" 53 | } 54 | }, 55 | 56 | comments: { 57 | 1: { 58 | id: 1, 59 | pathId: 1, 60 | authorId: 2, 61 | body: "good job!" 62 | } 63 | } 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /app/assets/stylesheets/friends/friends_tab.scss: -------------------------------------------------------------------------------- 1 | .friends-content-container { 2 | margin: 0 auto; 3 | max-width: 940px; 4 | padding-bottom: 100px; 5 | } 6 | 7 | .friends-tabs-container { 8 | 9 | .friends-tabs { 10 | display: flex; 11 | padding: 35px 0 20px 0; 12 | color: #046AB2; 13 | 14 | a { 15 | font-size: 14px; 16 | text-transform: uppercase; 17 | padding: 12px 23px; 18 | width: 500px; 19 | text-align: center; 20 | background: #f6f6f6; 21 | border-top: 1px solid #d1d0cf; 22 | border-bottom: 1px solid #d1d0cf; 23 | border-right: 1px solid #d1d0cf; 24 | font-weight: 700; 25 | 26 | 27 | &:hover { 28 | color: #92BFDD; 29 | background: #fff; 30 | } 31 | 32 | &:first-child { 33 | border-left: 1px solid #dddddd; 34 | } 35 | } 36 | 37 | .selected { 38 | border-top: 3.5px solid #2BA5EF; 39 | color: #444444; 40 | background: #fff; 41 | border-bottom: 0; 42 | cursor: default; 43 | 44 | &:hover { 45 | color: #444444; 46 | background: #fff; 47 | } 48 | } 49 | 50 | .end-border { 51 | border-bottom: 1px solid #d1d0cf; 52 | width: 100%; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MapMyPath 6 | <%= csrf_meta_tags %> 7 | 8 | 10 | 20 | 21 | <%= stylesheet_link_tag 'application', media: 'all' %> 22 | <%= javascript_include_tag 'application' %> 23 | <%= favicon_link_tag image_path("favicon.ico") %> 24 | 25 | 26 | 27 | 28 | 29 | <%= yield %> 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/components/home/dashboard/complete_paths_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import { convertSecondsToTime } from '../../../util/math_calculations'; 4 | 5 | const CompletePathsItem = ({ paths }) => { 6 | return ( 7 | 33 | ); 34 | }; 35 | 36 | export default CompletePathsItem; 37 | -------------------------------------------------------------------------------- /frontend/components/friends/friends_tab.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class FriendsTab extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.selectedTab = this.selectedTab.bind(this); 8 | } 9 | 10 | selectedTab(tab) { 11 | if (this.props.location.pathname.includes(tab)) { 12 | return "selected"; 13 | } 14 | return "unselected"; 15 | } 16 | 17 | render () { 18 | return ( 19 |
    20 |
    21 |
    22 |
    23 | 26 | My Friends 27 | 28 | 31 | Find Friends 32 | 33 |
    34 |
    35 |
    36 |
    37 | 38 | {this.props.children} 39 |
    40 | ); 41 | } 42 | } 43 | 44 | export default FriendsTab; 45 | -------------------------------------------------------------------------------- /frontend/components/friends/friend_requests_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class FriendRequestsIndex extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.friendListItem = this.friendListItem.bind(this); 8 | } 9 | 10 | friendListItem (friend) { 11 | return ( 12 |
  • 13 | 14 |
    15 |

    { friend.first_name + " " + friend.last_name }

    16 | 20 |
    21 |
  • 22 | ); 23 | } 24 | 25 | render () { 26 | if (this.props.friendRequests.length > 0) { 27 | return ( 28 |
    29 |
    30 |

    Friend Requests

    31 |
    32 |
      33 | {this.props.friendRequests.map(friend => this.friendListItem(friend))} 34 |
    35 |
    36 | ); 37 | } else { 38 | return
    ; 39 | } 40 | } 41 | } 42 | 43 | // return ( 44 | //
    hi
    45 | // ); 46 | export default FriendRequestsIndex ; 47 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | ## Component Hierarchy 2 | 3 | 4 | **NavigationContainer** 5 | - AuthFormContainer 6 | + AuthForm 7 | 8 | - HomePageContainer 9 | + ActivityFeedContainer 10 | - ActivityIndexContainer 11 | + ActivityIndexItem 12 | - RouteDetail 13 | + Comment 14 | + UserContainer 15 | 16 | + DashboardContainer 17 | - RoutesSummary 18 | 19 | + UserContainer 20 | 21 | + RoutesContainer 22 | - RoutesIndexContainer 23 | - RoutesIndexItems 24 | - NewRouteRouteForm 25 | 26 | - FriendsContainer 27 | + FriendsIndexContainer 28 | - FriendsIndexItem 29 | - PendingFriendsIndex 30 | - PendingFriendsIndexItem 31 | + FindFriendsSearchIndex 32 | - FindFriendsIndexItem 33 | 34 | 35 | 36 | ## Routes 37 | 38 | # Routes 39 | |Path | Component | 40 | | --------------------- | ------------- | 41 | | /signup | AuthFormContainer | 42 | | /login | AuthFormContainer | 43 | | /homepage/routes/ | RoutesIndexContainer | 44 | | /homepage/routes/new | NewRoutesForm | 45 | | /homepage/routes/:id | RouteShowContainer | 46 | | /homepage/dashboard/ | RoutesSummary | 47 | | /homepage/activity | ActivityFeedContainer | 48 | | /friends | FriendsContainer | 49 | | /friends/my-friends | FriendsIndexContainer | 50 | | /friends/find-friends | FindFriendsContainer | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MapMyPath", 3 | "version": "1.0.0", 4 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "postinstall": "webpack", 13 | "webpack": "./node_modules/.bin/webpack" 14 | }, 15 | "engines": { 16 | "node": "6.7.0", 17 | "npm": "3.10.7" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/eulow/MapMyPath.git" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/eulow/MapMyPath/issues" 28 | }, 29 | "homepage": "https://github.com/eulow/MapMyPath#readme", 30 | "dependencies": { 31 | "babel-core": "^6.24.1", 32 | "babel-loader": "^6.4.1", 33 | "babel-preset-es2015": "^6.24.1", 34 | "babel-preset-react": "^6.24.1", 35 | "react": "^15.5.4", 36 | "react-dom": "^15.5.4", 37 | "react-intl": "^2.2.3", 38 | "react-modal": "^1.7.7", 39 | "react-redux": "^5.0.4", 40 | "react-router": "^3.0.5", 41 | "redux": "^3.6.0", 42 | "redux-logger": "^3.0.1", 43 | "redux-thunk": "^2.2.0" 44 | }, 45 | "devDependencies": { 46 | "webpack": "^2.4.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/controllers/api/paths_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::PathsController < ApplicationController 2 | before_action :require_logged_in! 3 | 4 | def index 5 | if params[:type] == 'index' 6 | @paths = current_user.paths 7 | elsif params[:type] == 'activity' 8 | @paths = current_user.recent_activities 9 | end 10 | render :index 11 | end 12 | 13 | def create 14 | @path = current_user.paths.new(path_params) 15 | 16 | if @path.save 17 | render :show 18 | else 19 | render( 20 | json: @path.errors.messages, 21 | status: 422 22 | ) 23 | end 24 | end 25 | 26 | def show 27 | @path = Path.includes(:comments).find(params[:id]) 28 | render :show 29 | end 30 | 31 | def destroy 32 | @path = Path.find(params[:id]) 33 | @path.destroy 34 | render json: @path 35 | end 36 | 37 | def update 38 | @path = Path.find(params[:id]) 39 | 40 | if @path.update(path_params) 41 | render :show 42 | else 43 | render( 44 | json: @path.errors.messages, 45 | status: 422 46 | ) 47 | end 48 | end 49 | 50 | private 51 | 52 | def path_params 53 | params.require(:path).permit( 54 | :name, 55 | :polyline, 56 | :distance, 57 | :start_address, 58 | :end_address, 59 | :duration, 60 | :done, 61 | :done_date, 62 | :description 63 | ) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /frontend/components/home/home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class Home extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.selectedTab = this.selectedTab.bind(this); 8 | } 9 | 10 | selectedTab(tab) { 11 | if (this.props.location.pathname.includes(tab)) { 12 | return "selected"; 13 | } 14 | return "unselected"; 15 | } 16 | 17 | render () { 18 | return ( 19 |
    20 |
    21 |
    22 |
    23 | 26 | Activity Feed 27 | 28 | 31 | My Dashboard 32 | 33 | 35 | My Paths 36 | 37 |
    38 |
    39 |
    40 |
    41 | 42 | {this.props.children} 43 |
    44 | ); 45 | } 46 | } 47 | 48 | export default Home; 49 | -------------------------------------------------------------------------------- /frontend/components/home/paths/path_show/path_show_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import PathShow from './path_show'; 3 | 4 | import { 5 | createComment, 6 | deleteComment, 7 | requestAllComments 8 | } from '../../../../actions/comment_actions'; 9 | 10 | import { 11 | requestSinglePath, 12 | deletePath, 13 | updatePath 14 | } from '../../../../actions/path_actions'; 15 | 16 | import { clearErrors } from '../../../../actions/error_actions'; 17 | 18 | const mapStateToProps = ({ session, paths, comments, errors }) => { 19 | const path = paths[Object.keys(paths)]; 20 | return { 21 | path, 22 | errors, 23 | comments: Object.keys(comments).map(id => comments[id]) 24 | .sort(function(a, b) { return b.id - a.id; }), 25 | currentUser: session.currentUser 26 | }; 27 | }; 28 | 29 | const mapDispatchToProps = (dispatch) => { 30 | return { 31 | requestSinglePath: (id) => dispatch(requestSinglePath(id)), 32 | updatePath: (path) => dispatch(updatePath(path)), 33 | deletePath: (id) => dispatch(deletePath(id)), 34 | requestAllComments: (pathId) => dispatch(requestAllComments(pathId)), 35 | createComment: (commentId, pathId) => 36 | dispatch(createComment(commentId, pathId)), 37 | deleteComment: (commentId, pathId) => 38 | dispatch(deleteComment(commentId, pathId)), 39 | clearErrors: () => dispatch(clearErrors()) 40 | }; 41 | }; 42 | 43 | export default connect( 44 | mapStateToProps, 45 | mapDispatchToProps 46 | )(PathShow); 47 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as SessionAPIUtil from '../util/session_api_util'; 2 | import { clearErrors, addErrors } from './error_actions'; 3 | 4 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 5 | 6 | export const receiveCurrentUser = (currentUser) => ({ 7 | type: RECEIVE_CURRENT_USER, 8 | currentUser 9 | }); 10 | 11 | export const login = (user) => dispatch => { 12 | return ( 13 | SessionAPIUtil.login(user) 14 | .then( 15 | (currentUser) => { 16 | dispatch(receiveCurrentUser(currentUser)); 17 | dispatch(clearErrors()); 18 | 19 | // dispatches receiveCurrentUser session action and clear error action 20 | }, 21 | (errors) => { 22 | dispatch(addErrors(errors)); 23 | // dispatches error action to error reducer 24 | } 25 | ) 26 | ); 27 | }; 28 | 29 | export const signOut = () => dispatch => { 30 | return ( 31 | SessionAPIUtil.signOut() 32 | .then((currentUser) => { 33 | dispatch(receiveCurrentUser(null)); 34 | }) 35 | ); 36 | }; 37 | 38 | 39 | export const signUp = (user) => dispatch => ( 40 | SessionAPIUtil.signUp(user) 41 | .then( 42 | (currentUser) => { 43 | dispatch(receiveCurrentUser(currentUser)); 44 | dispatch(clearErrors()); 45 | 46 | // dispatches receiveCurrentUser session action and clear error action 47 | }, 48 | (errors) => { 49 | dispatch(addErrors(errors)); 50 | // dispatches error action to error reducer 51 | } 52 | ) 53 | ); 54 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_index/path_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import PathIndexItem from './path_index_item'; 4 | 5 | class PathIndex extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount () { 11 | this.props.requestAllPaths(); 12 | } 13 | 14 | render () { 15 | 16 | return ( 17 |
    18 |
    19 |

    My Paths

    20 | Create a path 24 | 25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | { 40 | this.props.paths.map(path => { 41 | return ( 42 | 46 | ); 47 | }) 48 | } 49 | 50 |
    PathCreatedDistanceNameStart LocationCompleteOptions
    51 |
    52 | ); 53 | } 54 | } 55 | 56 | export default PathIndex; 57 | -------------------------------------------------------------------------------- /frontend/actions/comment_actions.js: -------------------------------------------------------------------------------- 1 | import * as CommentsAPIUtil from '../util/comments_api_util'; 2 | import { clearErrors, addErrors } from './error_actions'; 3 | 4 | export const RECEIVE_ALL_COMMENTS = 'RECEIVE_ALL_COMMENTS'; 5 | export const RECEIVE_NEW_COMMENT = 'RECEIVE_NEW_COMMENT'; 6 | export const REMOVE_COMMENT = 'DELETE_COMMENT'; 7 | 8 | export const receiveAllComments = (comments) => ({ 9 | type: RECEIVE_ALL_COMMENTS, 10 | comments 11 | }); 12 | 13 | export const receiveNewComment = (comment) => ({ 14 | type: RECEIVE_NEW_COMMENT, 15 | comment 16 | }); 17 | 18 | export const removeComment = (id) => ({ 19 | type: REMOVE_COMMENT, 20 | id 21 | }); 22 | 23 | 24 | export const requestAllComments = (pathId) => (dispatch) => { 25 | return ( 26 | CommentsAPIUtil.getAllComments(pathId) 27 | .then( 28 | (comments) => { 29 | dispatch(receiveAllComments(comments)); 30 | } 31 | ) 32 | ); 33 | }; 34 | 35 | export const createComment = (comment) => (dispatch) => { 36 | return ( 37 | CommentsAPIUtil.createComment(comment) 38 | .then( 39 | (newComment) => { 40 | dispatch(receiveNewComment(newComment)); 41 | dispatch(clearErrors()); 42 | }, 43 | (errors) => { 44 | dispatch(addErrors(errors)); 45 | } 46 | ) 47 | ); 48 | }; 49 | 50 | export const deleteComment = (comment) => (dispatch) => { 51 | return ( 52 | CommentsAPIUtil.deleteComment(comment) 53 | .then( 54 | (deletedComment) => { 55 | dispatch(removeComment(deletedComment.id)); 56 | }, 57 | (errors) => { 58 | dispatch(addErrors(errors)); 59 | } 60 | ) 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/paths/map.scss: -------------------------------------------------------------------------------- 1 | #map-container { 2 | border: 1px solid black; 3 | position: relative; 4 | 5 | .map-buttons { 6 | position: absolute; 7 | z-index: 1; 8 | top: 10px; 9 | left: 100px; 10 | border-radius: 2px; 11 | font-family: Roboto, Arial, sans-serif; 12 | background-color: rgb(255, 255, 255); 13 | color: rgb(86, 86, 86); 14 | 15 | button { 16 | font-size: 11px; 17 | text-align: center; 18 | cursor: pointer; 19 | border-radius: 2px; 20 | padding: 8px; 21 | box-shadow: rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px; 22 | min-width: 30px; 23 | 24 | &:hover { 25 | background: -webkit-gradient( 26 | linear, left top, left bottom, 27 | from(#ededed), 28 | to(#e0e0e0)); 29 | } 30 | } 31 | } 32 | 33 | #map { 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: center; 37 | align-items: center; 38 | width: 690px; 39 | height: 620px; 40 | } 41 | } 42 | 43 | // .ReactModal__Body--open { 44 | // overflow: hidden; 45 | // } 46 | 47 | .map-modal-overlay { 48 | position: fixed; 49 | top: 0; 50 | bottom: 0; 51 | left: 0; 52 | right: 0; 53 | background-color: #dddddd; 54 | z-index: 3; 55 | opacity: .9; 56 | } 57 | 58 | .map-modal-content { 59 | border: 1px solid black; 60 | position: absolute; 61 | width: 300px; 62 | height: 300px; 63 | top: 26%; 64 | left: 40%; 65 | right: 50%; 66 | bottom: 50%; 67 | background: #fff; 68 | border-radius: 4px; 69 | overflow: hidden; 70 | 71 | div { 72 | margin: auto; 73 | } 74 | 75 | p{ 76 | text-align: center; 77 | padding: 40px; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /app/assets/stylesheets/friends/my_friends.scss: -------------------------------------------------------------------------------- 1 | .friends-main { 2 | display: flex; 3 | flex-direction: column; 4 | color: #444444; 5 | 6 | 7 | .friends, .friend-requests { 8 | header { 9 | margin-top: 8px; 10 | padding-bottom: 27px; 11 | border-bottom: 1px solid #d1d0cf; 12 | text-transform: uppercase; 13 | display: flex; 14 | text-transform: uppercase; 15 | justify-content: space-between; 16 | align-items: center; 17 | letter-spacing: 1px; 18 | 19 | h2 { 20 | font-weight: lighter; 21 | margin-left: 5px; 22 | font-weight: lighter; 23 | } 24 | 25 | .back-button { 26 | font-size: 12px; 27 | color: white; 28 | padding: 15px 18px; 29 | transition: .6s; 30 | border-radius: 2px; 31 | font-weight: bold; 32 | background: -webkit-gradient( 33 | linear, left top, left bottom, 34 | from(#4499db), 35 | to(#177ed7)); 36 | &:hover { 37 | background: -webkit-gradient( 38 | linear, left top, left bottom, 39 | from(#3f92c6), 40 | to(#0074bc)) 41 | } 42 | } 43 | } 44 | 45 | ul { 46 | display: flex; 47 | flex-wrap: wrap; 48 | margin: 30px 0; 49 | 50 | li { 51 | display: flex; 52 | width: 300px; 53 | height: 90px; 54 | align-items: center; 55 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 56 | background: #f8f8f8; 57 | margin: 0 10px 10px 0; 58 | 59 | img { 60 | height: 70px; 61 | width: 70px; 62 | margin: 10px; 63 | display: block; 64 | background: #fff; 65 | } 66 | 67 | button { 68 | color: #0076c0; 69 | font-size: 12px; 70 | cursor: pointer; 71 | padding: 2px 0; 72 | margin: 8px 20px 0 0; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/paths/paths_index.scss: -------------------------------------------------------------------------------- 1 | .paths-index { 2 | color: #444444; 3 | box-sizing:border-box; 4 | 5 | header { 6 | display: flex; 7 | text-transform: uppercase; 8 | justify-content: space-between; 9 | align-items: center; 10 | padding-bottom: 20px; 11 | border-bottom: 1px solid #d1d0cf; 12 | letter-spacing: 1px; 13 | 14 | .create-button { 15 | font-size: 12px; 16 | color: white; 17 | padding: 15px 18px; 18 | transition: .6s; 19 | border-radius: 2px; 20 | font-weight: bold; 21 | background: -webkit-gradient( 22 | linear, left top, left bottom, 23 | from(#ff9628), 24 | to(#ff8714)); 25 | &:hover { 26 | background: -webkit-gradient( 27 | linear, left top, left bottom, 28 | from(#f08a15), 29 | to(#e56e06)) 30 | } 31 | } 32 | .path-index-header { 33 | margin-left: 5px; 34 | font-weight: lighter; 35 | } 36 | } 37 | 38 | table { 39 | margin-top: 30px; 40 | width: 100%; 41 | font-size: 12px; 42 | border-collapse: collapse; 43 | 44 | th { 45 | padding: 15px 10px; 46 | text-align: left; 47 | border: 1px solid #d1d0cf; 48 | background-color: #f6f6f6; 49 | } 50 | td { 51 | padding: 10px 15px; 52 | border: 1px solid #d1d0cf; 53 | } 54 | 55 | td:first-child { 56 | padding: 10px 5px; 57 | 58 | img { 59 | display: block; 60 | margin: auto; 61 | } 62 | } 63 | 64 | tr:nth-child(odd) { 65 | background: #FFF 66 | } 67 | 68 | tr:nth-child(even) { 69 | background-color: #f6f6f6; 70 | } 71 | 72 | button { 73 | margin: auto; 74 | display: block; 75 | color: #0076c0; 76 | font-size: 12px; 77 | cursor: pointer; 78 | padding: 2px 0; 79 | } 80 | 81 | tbody tr:hover { 82 | background: #ececec; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/components/masthead/masthead.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | class Masthead extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.userInfo = this.userInfo.bind(this); 9 | this.signOut = this.signOut.bind(this); 10 | } 11 | 12 | sessionLinks () { 13 | return ( 14 | 18 | ); 19 | } 20 | 21 | signOut(e) { 22 | e.preventDefault(); 23 | this.props.signOut().then( 24 | () => { 25 | this.props.router.push('/'); 26 | } 27 | ); 28 | } 29 | 30 | userInfo () { 31 | return ( 32 | 43 | ); 44 | } 45 | 46 | 47 | render () { 48 | let render = this.props.currentUser ? this.userInfo() : this.sessionLinks(); 49 | if (this.props.path === '/login' || this.props.path ==='/signup') { 50 | render =
    ; 51 | } 52 | 53 | return ( 54 |
    55 | 56 |
    57 | 58 |

    mapmypath

    59 | 60 | 61 | {render} 62 |
    63 |
    64 | ); 65 | } 66 | } 67 | 68 | export default withRouter(Masthead); 69 | -------------------------------------------------------------------------------- /docs/README.MD: -------------------------------------------------------------------------------- 1 | # MapMyPath 2 | 3 | [Heroku Link][heroku] 4 | [Trello Link][trello] 5 | 6 | [heroku]: https://mapmypath.herokuapp.com/ 7 | [trello]: https://trello.com/b/CvrGYg5n 8 | 9 | ## Minimum Viable Product 10 | 11 | MapMyPath is a web application inspired by MapMyRun used for mapping 12 | your moving activities built using Ruby on Rails for the backend and 13 | React/Redux for the front end. By the end of week 9, this app will 14 | have the following complete allowing for a smooth, bug-free navigation with 15 | adequate seed data and CSS styling: 16 | 17 | - [ ] Hosting on Heroku 18 | - [ ] New account creation, login and guest login 19 | - [ ] Making running routes using a map 20 | - [ ] Friending other users 21 | - [ ] Commenting on your friends' runs 22 | - [ ] Dashboard that tracks all your pending/completed runs 23 | 24 | 25 | 26 | ## Design Docs 27 | 28 | * [View Wireframes][wireframes] 29 | * [React Components][components] 30 | * [API endpoints][api-points] 31 | * [DB Schema][schema] 32 | * [Sample State][sample-state] 33 | 34 | [wireframes]: wireframes 35 | [components]: component-hierarchy.md 36 | [api-points]: api-endpoints.md 37 | [schema]: schema.md 38 | [sample-state]: sample-state.md 39 | 40 | ## Implementation Timeline 41 | 42 | ### Phase 1: Backend setup and frontend user authentication (2 days) 43 | 44 | **Objective:** Functioning rail project with front-end authentication 45 | 46 | ### Phase 2: Dashboard (2 days) 47 | 48 | **Objective:** Dashboard that connects all 3 models together into one usable page, along with appropriate styling 49 | 50 | ### Phase 3: Maps model, API and components (2 days) 51 | 52 | **Objective:** Maps model that can be created, read, edited and destroyed through API, along with appropriate styling 53 | 54 | ### Phase 4: Friends model (2 days) 55 | 56 | **Objective:** Friends that belong to users that can be created, read, edited and destroyed, along with appropriate styling 57 | 58 | ### Phase 5: Comments model and Status model (1 day) 59 | 60 | **Objective:** Comments that belong to Users and Routes that can be created, read, edited and destroyed. 61 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | end 55 | -------------------------------------------------------------------------------- /app/controllers/api/relationships_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::RelationshipsController < ApplicationController 2 | before_action :require_logged_in! 3 | 4 | def index 5 | @friends = current_user.pending_friends 6 | render :index 7 | end 8 | 9 | def create 10 | @relationship = Relationship.new 11 | @relationship.action_user_id = current_user.id 12 | 13 | users = [current_user.id, params[:friend_id].to_i].sort 14 | 15 | @relationship.user_one_id = users[0] 16 | @relationship.user_two_id = users[1] 17 | @relationship.status = 0 18 | if @relationship.save 19 | @user = User.find(params[:friend_id]) 20 | render 'api/users/show' 21 | else 22 | render @relationship.errors.messages 23 | end 24 | end 25 | 26 | def show 27 | @friends = current_user.potential_friends(params[:search]) 28 | render :index 29 | end 30 | 31 | def update 32 | relationship = [current_user.id, params[:id].to_i].sort 33 | @relationship = Relationship 34 | .where(user_one_id: relationship[0]) 35 | .where(user_two_id: relationship[1]) 36 | .first 37 | 38 | @relationship.action_user_id = current_user.id 39 | @relationship.status = 1 40 | 41 | if @relationship.save 42 | friend_id = relationship.reject { |user| user == current_user.id } 43 | @user = User.find_by(id: friend_id.first) 44 | render 'api/users/show' 45 | else 46 | render json: @relationship.errors.messages 47 | end 48 | end 49 | 50 | def destroy 51 | friendship = [current_user.id, params[:id].to_i].sort 52 | @relationship = Relationship 53 | .where(user_one_id: friendship[0]) 54 | .where(user_two_id: friendship[1]) 55 | .first 56 | 57 | if @relationship.destroy 58 | friend_id = friendship.reject { |user| user == current_user.id } 59 | @user = User.find_by(id: friend_id.first) 60 | render 'api/users/show' 61 | else 62 | render json: @relationship.errors.messages 63 | end 64 | end 65 | 66 | private 67 | 68 | def relationship_params 69 | params.require(:relationship).permit(:friend_id) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | gem 'faker' 9 | 10 | gem 'bcrypt' 11 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 12 | gem 'figaro' 13 | 14 | gem 'rails', '~> 5.0.2' 15 | # Use postgresql as the database for Active Record 16 | gem 'pg', '~> 0.18' 17 | # Use Puma as the app server 18 | gem 'puma', '~> 3.0' 19 | # Use SCSS for stylesheets 20 | gem 'sass-rails', '~> 5.0' 21 | # Use Uglifier as compressor for JavaScript assets 22 | gem 'uglifier', '>= 1.3.0' 23 | # Use CoffeeScript for .coffee assets and views 24 | gem 'coffee-rails', '~> 4.2' 25 | # See https://github.com/rails/execjs#readme for more supported runtimes 26 | # gem 'therubyracer', platforms: :ruby 27 | 28 | # Use jquery as the JavaScript library 29 | gem 'jquery-rails' 30 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 31 | gem 'jbuilder', '~> 2.5' 32 | # Use Redis adapter to run Action Cable in production 33 | # gem 'redis', '~> 3.0' 34 | # Use ActiveModel has_secure_password 35 | # gem 'bcrypt', '~> 3.1.7' 36 | 37 | # Use Capistrano for deployment 38 | # gem 'capistrano-rails', group: :development 39 | 40 | group :development, :test do 41 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 42 | gem 'annotate' 43 | gem 'better_errors' 44 | gem 'binding_of_caller' 45 | gem 'byebug', platform: :mri 46 | gem 'pry-rails' 47 | end 48 | 49 | group :development do 50 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 51 | gem 'listen', '~> 3.0.5' 52 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 53 | gem 'spring' 54 | gem 'spring-watcher-listen', '~> 2.0.0' 55 | gem 'web-console', '>= 3.3.0' 56 | end 57 | 58 | group :production do 59 | gem 'rails_12factor' 60 | end 61 | 62 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 63 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 64 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/friends/friend_search.scss: -------------------------------------------------------------------------------- 1 | .friends-search { 2 | color: #444444; 3 | 4 | // * { 5 | // border: 1px solid black; 6 | // } 7 | 8 | header { 9 | margin-top: 8px; 10 | padding-bottom: 27px; 11 | border-bottom: 1px solid #d1d0cf; 12 | // vertical-align: middle; 13 | 14 | h2 { 15 | text-transform: uppercase; 16 | letter-spacing: 1px; 17 | font-weight: lighter; 18 | margin-left: 5px; 19 | } 20 | 21 | form { 22 | margin-top: 10px; 23 | 24 | input[type='text'] { 25 | border: 1px solid #d1d0cf; 26 | width: 500px; 27 | margin: 10px 0 0 5px; 28 | font-size: 14px; 29 | padding: 10px;; 30 | } 31 | 32 | input[type='submit'] { 33 | margin-left: 20px; 34 | text-transform: uppercase; 35 | font-size: 12px; 36 | color: white; 37 | padding: 15px 60px; 38 | transition: .6s; 39 | border-radius: 2px; 40 | font-weight: bold; 41 | letter-spacing: .5px; 42 | background: -webkit-gradient( 43 | linear, left top, left bottom, 44 | from(#ff9628), 45 | to(#ff8714)); 46 | cursor: pointer; 47 | &:hover { 48 | background: -webkit-gradient( 49 | linear, left top, left bottom, 50 | from(#f08a15), 51 | to(#e56e06)) 52 | } 53 | } 54 | } 55 | } 56 | 57 | .unable-to-find { 58 | margin: 30px 0 0 10px; 59 | } 60 | 61 | ul { 62 | display: flex; 63 | flex-wrap: wrap; 64 | margin: 30px 0; 65 | 66 | li { 67 | display: flex; 68 | width: 300px; 69 | height: 90px; 70 | align-items: center; 71 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 72 | background: #f8f8f8; 73 | margin: 0 10px 10px 0; 74 | 75 | img { 76 | height: 70px; 77 | width: 70px; 78 | margin: 10px; 79 | display: block; 80 | background: #fff; 81 | } 82 | 83 | button { 84 | color: #0076c0; 85 | font-size: 12px; 86 | cursor: pointer; 87 | padding: 2px 0; 88 | margin: 8px 20px 0 0; 89 | } 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /frontend/components/friends/friend_search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class FriendSearch extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.search = ''; 7 | 8 | this.state = { 9 | search: '' 10 | }; 11 | 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | this.renderSearchResults = this.renderSearchResults.bind(this); 14 | } 15 | 16 | handleSubmit(e) { 17 | e.preventDefault(); 18 | this.search = this.state.search; 19 | 20 | this.props.requestAllPotentialFriends(this.state.search); 21 | } 22 | 23 | update(field) { 24 | return e => this.setState({ 25 | [field]: e.currentTarget.value 26 | }); 27 | } 28 | 29 | renderSearchResults() { 30 | if (this.props.potentialFriends.length > 0) { 31 | return ( 32 | 45 | ); 46 | } else if (this.props.potentialFriends.length === 0 && this.search) { 47 | return

    We couldn't find anything for {this.search}.

    ; 48 | } 49 | } 50 | 51 | render () { 52 | 53 | return ( 54 |
    55 |
    56 |

    Find MapMyPath friends by first name, last name, or email:

    57 |
    58 | 64 | 65 |
    66 |
    67 | {this.renderSearchResults()} 68 |
    69 | ); 70 | } 71 | } 72 | 73 | export default FriendSearch; 74 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # Schema Information 2 | 3 | ## users 4 | 5 | | column name | data type | details | 6 | | ------------- | ------------- | ------------- | 7 | | id | integer | not null, primary key | 8 | | email | string | not null, indexed, unique | 9 | | first_name | string | not null, indexed | 10 | | last_name | string | not null, indexed | 11 | | password_digest | string | not null | 12 | | session_token | string | not null, indexed, unique | 13 | | img_url | string | | 14 | | created_at | timestamp | not null | 15 | | updated_at | timestamp | not null | 16 | 17 | ## routes 18 | 19 | | column name | data type | details | 20 | | ------------- | ---------- | ------------- | 21 | | id | integer | not null, primary key | 22 | | user_id | integer | not null, foreign key | 23 | | name | string | not null | 24 | | polyline | string | not null | 25 | | start_address | string | not null | 26 | | end_address | string | not null | 27 | | distance | float | not null | 28 | | done | boolean | not null, default: false | 29 | | created_at | timestamp | not null | 30 | | updated_at | timestamp | not null | 31 | 32 | ## relationships 33 | 34 | | column name | data type | details | 35 | | ------------- | ----------| ------------- | 36 | | id | integer | not null, primary key | 37 | | user_one_id | integer | not null, foreign key | 38 | | user_two_id | integer | not null, foreign key | 39 | | status | integer | not null | 40 | | action_user_id | integer | not null, foreign key | 41 | | created_at | timestamp | not null | 42 | | updated_at | timestamp | not null | 43 | 44 | ## comments 45 | 46 | | column name | data type | details | 47 | | ------------- | ----------- | ------------- | 48 | | id | integer | not null, primary key | 49 | | author_id | integer | not null, foreign key | 50 | | path_id | integer | not null, foreign key | 51 | | body | text | not null | 52 | | created_at | timestamp | not null | 53 | | updated_at | timestamp | not null | 54 | -------------------------------------------------------------------------------- /frontend/components/home/paths/path_show/comments.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CommentIndexItem from './comment_index_item'; 3 | 4 | class Comments extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.handleSubmit = this.handleSubmit.bind(this); 9 | 10 | this.state = { 11 | body: "" 12 | }; 13 | } 14 | 15 | update(field) { 16 | return (e) => { 17 | this.setState({ 18 | [field]: e.currentTarget.value 19 | }); 20 | }; 21 | } 22 | 23 | handleSubmit(e) { 24 | e.preventDefault(); 25 | const comment = Object.assign({}, this.state, { path_id: this.props.path.id }); 26 | this.setState( 27 | { body: "" } 28 | ); 29 | this.props.createComment(comment); 30 | } 31 | 32 | renderError({ body }) { 33 | if(body) { 34 | return ( 35 |
    Comment {body}.
    36 | ); 37 | } 38 | } 39 | 40 | render () { 41 | const { 42 | comments, 43 | createComment, 44 | deleteComment, 45 | currentUser, 46 | errors, 47 | path 48 | } = this.props; 49 | 50 | return ( 51 |
    52 |

    Comments

    53 |
    54 | 55 |
    56 | 63 |
    64 | 65 |
    66 | {this.renderError(errors)} 67 |
      68 | { 69 | comments.map(comment => { 70 | return ( 71 | 77 | ); 78 | }) 79 | } 80 |
    81 |
    82 | ); 83 | } 84 | } 85 | 86 | export default Comments; 87 | -------------------------------------------------------------------------------- /frontend/actions/path_actions.js: -------------------------------------------------------------------------------- 1 | import * as PathsAPIUtil from '../util/paths_api_util'; 2 | import { clearErrors, addErrors } from './error_actions'; 3 | 4 | export const RECEIVE_ALL_PATHS = 'RECEIVE_ALL_PATHS'; 5 | export const RECEIVE_SINGLE_PATH = 'RECEIVE_SINGLE_PATH'; 6 | export const RECEIVE_NEW_PATH = 'RECEIVE_NEW_PATH'; 7 | export const REMOVE_PATH = 'REMOVE_PATH'; 8 | export const CLEAR_PATHS = 'CLEAR_PATHS'; 9 | 10 | export const receiveAllPaths = (paths) => ({ 11 | type: RECEIVE_ALL_PATHS, 12 | paths 13 | }); 14 | 15 | export const clearPaths = () => ({ 16 | type: CLEAR_PATHS 17 | }); 18 | 19 | export const receiveSinglePath = (path) => ({ 20 | type: RECEIVE_SINGLE_PATH, 21 | path 22 | }); 23 | 24 | export const receiveNewPath = (path) => ({ 25 | type: RECEIVE_NEW_PATH, 26 | path 27 | }); 28 | 29 | export const removePath = (id) => ({ 30 | type: REMOVE_PATH, 31 | id 32 | }); 33 | 34 | export const requestAllPaths = (type) => (dispatch) => { 35 | return ( 36 | PathsAPIUtil.getAllPaths(type) 37 | .then( 38 | (paths) => { 39 | dispatch(receiveAllPaths(paths)); 40 | } 41 | ) 42 | ); 43 | }; 44 | 45 | export const requestSinglePath = (id) => (dispatch) => { 46 | return ( 47 | PathsAPIUtil.getSinglePath(id) 48 | .then( 49 | (path) => { 50 | dispatch(receiveSinglePath(path)); 51 | } 52 | ) 53 | ); 54 | }; 55 | 56 | 57 | export const createPath = (path) => (dispatch) => { 58 | // dispatch(receiveNewPath(newPath)); 59 | // not necessary since it will redirect to new page, 60 | // thus loading new component 61 | 62 | return ( 63 | PathsAPIUtil.createPath(path) 64 | .then( 65 | (newPath) => { 66 | dispatch(clearErrors()); 67 | return newPath; 68 | }, 69 | (errors) => { 70 | dispatch(addErrors(errors)); 71 | } 72 | ) 73 | ); 74 | }; 75 | 76 | export const updatePath = (path) => (dispatch) => { 77 | return ( 78 | PathsAPIUtil.updatePath(path) 79 | .then( 80 | (updatedPath) => { 81 | dispatch(receiveSinglePath(updatedPath)); 82 | dispatch(clearErrors()); 83 | }, 84 | (errors) => { 85 | dispatch(addErrors(errors)); 86 | } 87 | ) 88 | ); 89 | }; 90 | 91 | export const deletePath = (id) => (dispatch) => { 92 | return ( 93 | PathsAPIUtil.deletePath(id) 94 | .then( 95 | (deletedPath) => { 96 | dispatch(removePath(deletedPath.id)); 97 | }, 98 | (errors) => { 99 | dispatch(addErrors(errors)); 100 | } 101 | ) 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /frontend/components/frontpage/frontpage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import { login } from '../../actions/session_actions'; 5 | 6 | class Frontpage extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.loginGuest = this.loginGuest.bind(this); 11 | } 12 | 13 | loginGuest () { 14 | const guest = { 15 | email: "Guest", 16 | password: "Password" 17 | }; 18 | this.props.login(guest).then(() => this.props.router.push('/home')); 19 | } 20 | 21 | render () { 22 | return( 23 |
    24 |
    25 |
    26 |

    Make every mile count, join free today

    27 |

    You pound the pavement, we provide the motivation. 28 | Plan each stride and learn from every route with MapMyPath. 29 |

    30 | 37 |
    38 |

    Already a member?

    39 | Log in 40 |
    41 |
    42 |
    43 |
    44 |
    45 |
    46 |
    47 |

    Map your route

    48 |

    Know where you're going, see where you've been. Be bold and create your own runs.

    49 |
    50 |
    51 |
    52 |

    Track your activity

    53 |

    Record your activity, track your speed, track your runs.

    54 |
    55 |
    56 |
    57 |

    Share with friends

    58 |

    Add a social twist to your exercise routine. Get extra encourgement, cheer on your buddies.

    59 |
    60 | 61 |
    62 |
    63 |
    64 | ); 65 | } 66 | } 67 | 68 | const mapDispatchToProps = (dispatch) => { 69 | return { 70 | login: (guest) => dispatch(login(guest)) 71 | }; 72 | }; 73 | 74 | export default connect( 75 | null, 76 | mapDispatchToProps 77 | )(Frontpage); 78 | -------------------------------------------------------------------------------- /frontend/components/home/activity_feed/activity_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import { FormattedRelative } from 'react-intl'; 4 | 5 | const ActivityIndexItem = ({ paths }) => { 6 | const activityRender = (activity) => { 7 | const map = `https://maps.googleapis.com/maps/api/staticmap?size=278x160&path=weight:3%7Ccolor:red%7Cenc:${ activity.polyline }&key=AIzaSyBygQhRnDSS9s1hu7jxsQMu3mwIU7Hd2N4`; if (activity.done) { 8 | return ( 9 |
  • 10 | 11 |
    12 | 13 |

    { activity.user.name} ran { activity.distance } miles

    14 | 15 |
    16 | 17 |
    18 | 19 |
    20 |
    21 | 22 |

    distance

    23 |

    { activity.distance } mi

    24 |
    25 | 26 |

    27 |
    28 |
  • 29 | ); 30 | } else { 31 | return ( 32 |
  • 33 | 34 |
    35 | 36 |

    { activity.user.name} created the { activity.name } path

    37 | 38 |
    39 | 40 |
    41 | 42 |

    distance

    43 |

    { activity.distance }

    mi 44 |
    45 |
    46 | 47 |
    48 | 49 |

    50 |
    51 |
  • 52 | ); 53 | } 54 | }; 55 | 56 | return ( 57 | 64 | ); 65 | }; 66 | 67 | 68 | export default ActivityIndexItem; 69 | -------------------------------------------------------------------------------- /frontend/components/home/dashboard/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PendingPathsItem from './pending_paths_index_item'; 3 | import CompletePathsItem from './complete_paths_index_item'; 4 | import Profile from '../profile_container'; 5 | import { convertSecondsToTime } from '../../../util/math_calculations'; 6 | 7 | class Dashboard extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.totalDistance = this.totalDistance.bind(this); 12 | this.totalTime = this.totalTime.bind(this); 13 | } 14 | 15 | componentWillMount () { 16 | this.props.requestAllPaths(); 17 | } 18 | 19 | componentWillUnmount () { 20 | this.props.clearPaths(); 21 | } 22 | 23 | totalDistance () { 24 | if (this.props.completePaths.length > 0) { 25 | let result = 0; 26 | for (let i = 0; i < this.props.completePaths.length; i++) { 27 | result += this.props.completePaths[i].distance; 28 | } 29 | return result.toFixed(2); 30 | } 31 | 32 | return 0; 33 | } 34 | 35 | totalTime () { 36 | if (this.props.completePaths.length > 0) { 37 | let result = 0; 38 | for (let i = 0; i < this.props.completePaths.length; i++) { 39 | result += this.props.completePaths[i].duration; 40 | } 41 | return convertSecondsToTime(result); 42 | } 43 | return "00:00:00"; 44 | } 45 | 46 | render () { 47 | return ( 48 |
    49 |
    50 |
    51 |

    52 | life time stats 53 |

    54 |
    55 |
    56 | 57 |

    Distance

    58 |

    { this.totalDistance() }

    59 |
    miles
    60 |
    61 | 62 |

    Time

    63 |

    { this.totalTime() }

    64 |
    hh : mm : ss
    65 |
    66 | 67 |

    Runs

    68 |

    { this.props.completePaths.length }

    69 |
    completed
    70 |
    71 |
    72 |
    73 |

    Pending runs

    74 | 75 |
    76 |
    77 |

    Completed runs

    78 | 79 |
    80 |
    81 | 82 |
    83 | ); 84 | } 85 | } 86 | 87 | //
    88 | //
    89 | 90 | export default Dashboard; 91 | -------------------------------------------------------------------------------- /app/assets/stylesheets/session_form/session_form.scss: -------------------------------------------------------------------------------- 1 | .auth-form-container { 2 | display: flex; 3 | justify-content: center; 4 | background: #f0f0f0; 5 | // height: 600px; 6 | border-bottom: 1px solid #d6d6d6; 7 | 8 | // * { 9 | // border: 1px solid black; 10 | // } 11 | .errors { 12 | font-size: 12px; 13 | color: rgba(255, 0, 0, 0.69); 14 | padding: 5px 0 3px 8px; 15 | } 16 | 17 | form { 18 | display: flex; 19 | flex-direction: column; 20 | position: relative; 21 | top: 5%; 22 | // height: 600px; 23 | width: 320px; 24 | margin: 40px 0 100px; 25 | 26 | } 27 | 28 | a.toggle-button { 29 | position: relative; 30 | padding: 10px 0; 31 | margin: 10px 0 0 0; 32 | top: 2px; 33 | text-transform: uppercase; 34 | font-family: sans-serif; 35 | font-size: 12px; 36 | color: #9297a3; 37 | font-weight: 500; 38 | text-align: right; 39 | transition-duration: .4s; 40 | 41 | &:hover { 42 | color: #6d717a; 43 | font-weight: 500 44 | } 45 | } 46 | 47 | 48 | .or-bar { 49 | display: flex; 50 | align-items: center; 51 | margin: 15px 0; 52 | 53 | .bar { 54 | display: block; 55 | height: 1px; 56 | flex: 1; 57 | border-bottom: 1px solid #c8c7cc 58 | } 59 | 60 | .or { 61 | text-transform: uppercase; 62 | font-size: 12px; 63 | padding: 0 15px; 64 | color: #c8c7cc; 65 | } 66 | } 67 | 68 | input[type="submit"], .guest-login { 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | background: #97bf11; 73 | color: #fff; 74 | width: 100%; 75 | height: 24px; 76 | cursor: pointer; 77 | transition-duration: .4s; 78 | text-transform: uppercase; 79 | margin: 15px 0 5px 0; 80 | padding: 10px 0; 81 | font-size: 12px; 82 | letter-spacing: .8px; 83 | border-radius: 6px; 84 | font-weight: 600; 85 | &:hover { 86 | background-color: #94bb0f 87 | } 88 | } 89 | 90 | .guest-login { 91 | background-color: #3b5998; 92 | &:hover { 93 | background-color: #274178; 94 | } 95 | } 96 | 97 | .input-container { 98 | display: block; 99 | margin-bottom: 4px; 100 | 101 | height: 40px; 102 | margin: 6px 0; 103 | } 104 | 105 | .input-box { 106 | border: 1px solid #c8c7cc; 107 | display: block; 108 | width: 100%; 109 | padding: 20px 16px; 110 | height: 44px; 111 | box-sizing: border-box; 112 | font-size: 12px; 113 | font-weight: 100; 114 | &::-webkit-input-placeholder { 115 | color: #c8c7cc; 116 | } 117 | &:focus { 118 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 119 | background: #fff; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/activity/activity.scss: -------------------------------------------------------------------------------- 1 | .activities { 2 | display: flex; 3 | color: #444444; 4 | 5 | .activities-body { 6 | min-width: 664px; 7 | 8 | header { 9 | margin-top: 8px; 10 | padding-bottom: 27px; 11 | border-bottom: 1px solid #d1d0cf; 12 | 13 | h2 { 14 | text-transform: uppercase; 15 | letter-spacing: 1px; 16 | font-weight: lighter; 17 | margin-left: 5px; 18 | font-weight: lighter; 19 | } 20 | } 21 | } 22 | 23 | li { 24 | display: flex; 25 | margin-top: 30px; 26 | 27 | .avatar { 28 | width: 65px; 29 | height: 60px; 30 | background: #fff; 31 | border: 1px solid #cdcdcd; 32 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 33 | } 34 | 35 | .activity-body { 36 | width: 575px; 37 | margin-left: 20px; 38 | border: 1px solid #cdcdcd; 39 | font-weight: lighter; 40 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); 41 | 42 | span { 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | padding: 10px 18px; 47 | background: #fff; 48 | 49 | p { 50 | letter-spacing: .6px; 51 | font-size: 14px; 52 | font-weight: lighter; 53 | } 54 | .fa-activity { 55 | color: #e6e6e6; 56 | } 57 | } 58 | 59 | a { 60 | display: flex; 61 | justify-content: space-between; 62 | background: #f8f8f8; 63 | border-top: 1px solid #cdcdcd; 64 | border-bottom: 1px solid #cdcdcd; 65 | 66 | div:first-child{ 67 | border-right: 1px solid #cdcdcd; 68 | } 69 | 70 | img { 71 | display: block; 72 | } 73 | 74 | .activity-details { 75 | width: 100%; 76 | text-align: center; 77 | margin: auto; 78 | padding: 26px 0; 79 | } 80 | 81 | .fa-road { 82 | color: #07b1c1; 83 | font-size: 25px; 84 | } 85 | 86 | p { 87 | text-transform: uppercase; 88 | font-size: 14px; 89 | margin-top: 10px; 90 | color: #a7a7a7; 91 | } 92 | 93 | .distance { 94 | font-size: 24px; 95 | color: #666666; 96 | } 97 | } 98 | 99 | .bottom { 100 | display: flex; 101 | align-items: center; 102 | justify-content: flex-end; 103 | color: #9d9d9d; 104 | font-size: 10px; 105 | height: 35px; 106 | background: #f8f8f8; 107 | 108 | span { 109 | margin: 0; 110 | padding:10px; 111 | background: #f8f8f8; 112 | } 113 | 114 | .fa-user-o { 115 | padding-right: 10px; 116 | border-right: 1px solid #9d9d9d; 117 | } 118 | 119 | } 120 | } 121 | 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # email :string not null 7 | # first_name :string not null 8 | # last_name :string not null 9 | # password_digest :string not null 10 | # session_token :string not null 11 | # img_url :string default("https://s3.us-east-2.amazonaws.com/mapmyrun-dev/default_avatar.png") 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # 15 | 16 | class User < ApplicationRecord 17 | validates :email, :session_token, presence: true, uniqueness: true 18 | validates :first_name, :last_name, presence: true 19 | validates :password, length: { minimum: 6, allow_nil: true } 20 | 21 | attr_reader :password 22 | 23 | after_initialize :ensure_session_token 24 | 25 | has_many :paths, dependent: :destroy 26 | has_many :comments, 27 | class_name: 'Comment', 28 | primary_key: :id, 29 | foreign_key: :author_id, 30 | dependent: :destroy 31 | 32 | def friends 33 | relationship_where_status(1) 34 | end 35 | 36 | def pending_friends 37 | relationship_where_status(0).where('relationships.action_user_id != :id', id: self.id) 38 | end 39 | 40 | def potential_friends(search) 41 | not_potential_friends = Relationship.where('user_one_id = :id or user_two_id = :id', id: self.id) 42 | ids = not_potential_friends.pluck(:user_one_id).concat(not_potential_friends.pluck(:user_two_id)).uniq 43 | return User.where.not(id: ids).where('first_name ILIKE :search OR last_name ILIKE :search OR email ILIKE :search', search: "%#{search}%") 44 | end 45 | 46 | def recent_activities 47 | ids = self.friends.ids 48 | ids.push(self.id).uniq! 49 | Path.where(user_id: ids).includes(:comments) 50 | end 51 | 52 | def self.find_by_credentials(email, password) 53 | user = User.find_by(email: email) 54 | return nil unless user && user.valid_password?(password) 55 | user 56 | end 57 | 58 | def password=(password) 59 | @password = password 60 | self.password_digest = BCrypt::Password.create(password) 61 | end 62 | 63 | def valid_password?(password) 64 | BCrypt::Password.new(self.password_digest).is_password?(password) 65 | end 66 | 67 | def reset_session_token! 68 | self.session_token = SecureRandom.urlsafe_base64(16) 69 | self.save! 70 | self.session_token 71 | end 72 | 73 | private 74 | 75 | def ensure_session_token 76 | self.session_token ||= SecureRandom.urlsafe_base64(16) 77 | end 78 | 79 | def relationship_where_status(status) 80 | User 81 | .joins('JOIN relationships ON relationships.user_one_id = users.id OR relationships.user_two_id = users.id') 82 | .where("relationships.status = :status", status: status) 83 | .where('relationships.user_one_id = :id OR relationships.user_two_id = :id', id: self.id) 84 | .where('users.id != :id', id: self.id) 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /app/assets/stylesheets/masthead/masthead.scss: -------------------------------------------------------------------------------- 1 | .masthead-container { 2 | margin: 0 auto; 3 | border-bottom: 1px solid #d6d6d6; 4 | justify-content: center; 5 | background: #fff; 6 | 7 | .masthead { 8 | margin: auto; 9 | display: flex; 10 | height: 70px; 11 | justify-content: space-between; 12 | max-width: 950px; 13 | // padding: 0 50px; 14 | // padding-right: 100px; 15 | // left: 50%; 16 | 17 | .logo { 18 | display: flex; 19 | align-items: center; 20 | margin-bottom: 4px; 21 | 22 | .title { 23 | font-size: 26px; 24 | padding: 5px; 25 | font-weight: 700; 26 | color: #2f98d9; 27 | // letter-spacing: 1px; 28 | 29 | &:before { 30 | position: relative; 31 | top: 2px; 32 | padding: 0 15px 0 0; 33 | content: "∆"; 34 | font-size: 30px; 35 | font-weight: 400; 36 | } 37 | } 38 | } 39 | 40 | .user-info { 41 | position: relative; 42 | .avatar { 43 | position: relative; 44 | height: 42px; 45 | width: 45px; 46 | border: .5px solid #d1d0cf; 47 | margin: 0 8px 0 0; 48 | } 49 | 50 | &:hover #drop-down-menu { 51 | display: flex; 52 | } 53 | 54 | #drop-down-menu { 55 | display: none; 56 | flex-direction: column; 57 | position: absolute; 58 | border: 1px solid #d3d6dc; 59 | border-top: 0; 60 | color: #9297a3; 61 | box-shadow: 0 2px 4px 0 rgba(34, 38, 40, 0.2); 62 | background-color: #fff; 63 | top: 70px; 64 | right: 6px; 65 | z-index: 100; 66 | 67 | .drop-down-button { 68 | text-align: center; 69 | padding: 16px 20px; 70 | font-size: 12px; 71 | border-top: 1px solid #d3d6dc; 72 | cursor: pointer; 73 | font-weight: 500; 74 | white-space: nowrap; 75 | 76 | &:hover { 77 | color: black; 78 | } 79 | } 80 | } 81 | } 82 | 83 | 84 | nav { 85 | display: flex; 86 | align-items: center; 87 | 88 | .login-button, .signup-button, .signout-button { 89 | position: relative; 90 | padding: 10px; 91 | margin: 5px; 92 | text-transform: uppercase; 93 | font-size: 13px; 94 | color: #9297a3; 95 | transition-duration: .4s; 96 | } 97 | 98 | .signout-button { 99 | margin: 0 0 0 10px; 100 | cursor: pointer; 101 | color: #fff; 102 | border-radius: 5px; 103 | background-color: #3b5998; 104 | 105 | &:hover { 106 | background-color: #274178; 107 | } 108 | } 109 | 110 | a.signup-button { 111 | background: #97bf11; 112 | color: #fff; 113 | border-radius: 5px; 114 | } 115 | 116 | a.signup-button:hover { 117 | background: #94bb0f; 118 | } 119 | 120 | a.login-button:hover { 121 | color: #6d717a; 122 | font-weight: 500 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /frontend/actions/friend_actions.js: -------------------------------------------------------------------------------- 1 | import * as FriendsAPIUtil from '../util/friends_api_util'; 2 | 3 | export const RECEIVE_ALL_FRIENDS = 'RECEIVE_ALL_FRIENDS'; 4 | export const RECEIVE_ALL_REQUESTS = 'RECEIVE_ALL_REQUESTS'; 5 | export const RECEIVE_NEW_FRIEND = 'RECEIVE_NEW_FRIEND'; 6 | export const REMOVE_FRIEND = 'REMOVE_FRIEND'; 7 | export const REMOVE_REQUEST = 'DENY_REQUEST'; 8 | export const RECEIVE_ALL_POTENTIAL_FRIENDS = 'RECEIVE_ALL_POTENTIAL_FRIENDS'; 9 | export const REMOVE_FROM_SEARCH = 'REMOVE_FROM_SEARCH'; 10 | 11 | export const receiveAllFriends = (friends) => ({ 12 | type: RECEIVE_ALL_FRIENDS, 13 | friends 14 | }); 15 | 16 | export const receiveAllRequests = (friends) => ({ 17 | type: RECEIVE_ALL_REQUESTS, 18 | friends 19 | }); 20 | 21 | export const receiveNewFriend = (friend) => ({ 22 | type: RECEIVE_NEW_FRIEND, 23 | friend 24 | }); 25 | 26 | export const removeFriend = (id) => ({ 27 | type: REMOVE_FRIEND, 28 | id 29 | }); 30 | 31 | export const removeRequest = (friend) => ({ 32 | type: REMOVE_REQUEST, 33 | friend 34 | }); 35 | 36 | export const receiveAllPotentialFriends = (friends) => ({ 37 | type: RECEIVE_ALL_POTENTIAL_FRIENDS, 38 | friends 39 | }); 40 | 41 | export const removeFromSearch = (friend) => ({ 42 | type: REMOVE_FROM_SEARCH, 43 | friend 44 | }); 45 | 46 | export const requestAllFriends = () => (dispatch) => { 47 | return ( 48 | FriendsAPIUtil.getAllFriends() 49 | .then( 50 | (friends) => { 51 | dispatch(receiveAllFriends(friends)); 52 | } 53 | ) 54 | ); 55 | }; 56 | 57 | export const requestAllRequests = () => (dispatch) => { 58 | return ( 59 | FriendsAPIUtil.getPendingFriends() 60 | .then( 61 | (friends) => { 62 | dispatch(receiveAllRequests(friends)); 63 | } 64 | ) 65 | ); 66 | }; 67 | 68 | export const addFriend = (id) => (dispatch) => { 69 | return ( 70 | FriendsAPIUtil.addFriend(id) 71 | .then( 72 | (friend) => { 73 | dispatch(receiveNewFriend(friend)); 74 | dispatch(removeRequest(friend)); 75 | } 76 | ) 77 | ); 78 | }; 79 | 80 | export const deleteFriend = (friendId) => (dispatch) => { 81 | return ( 82 | FriendsAPIUtil.deleteFriend(friendId) 83 | .then( 84 | (deletedFriend) => { 85 | dispatch(removeFriend(deletedFriend.id)); 86 | } 87 | ) 88 | ); 89 | }; 90 | 91 | export const deleteRequest = (friendId) => (dispatch) => { 92 | return ( 93 | FriendsAPIUtil.deleteRequest(friendId) 94 | .then( 95 | (deletedFriend) => { 96 | dispatch(removeRequest(deletedFriend)); 97 | } 98 | ) 99 | ); 100 | }; 101 | 102 | export const addRequest = (friendId) => (dispatch) => { 103 | return ( 104 | FriendsAPIUtil.addRequest(friendId) 105 | .then( 106 | (friend) => { 107 | dispatch(removeFromSearch(friend)); 108 | } 109 | ) 110 | ); 111 | }; 112 | 113 | export const requestAllPotentialFriends = (search) => (dispatch) => { 114 | return ( 115 | FriendsAPIUtil.getPotentialFriends(search) 116 | .then( 117 | (potentialFriends) => { 118 | dispatch(receiveAllPotentialFriends(potentialFriends)); 119 | } 120 | ) 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: MapMyPath_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: MapMyPath 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: MapMyPath_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: MapMyPath_production 84 | username: MapMyPath 85 | password: <%= ENV['MAPMYPATH_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_form/map_manager.js: -------------------------------------------------------------------------------- 1 | export default class MapManager { 2 | constructor(map, updateState) { 3 | this.map = map; 4 | this.updateState = updateState; 5 | this.pathMarkers = []; 6 | 7 | this.directionsService = new google.maps.DirectionsService; 8 | this.directionsRenderer = new google.maps.DirectionsRenderer( 9 | { 10 | map: this.map, 11 | preserveViewport: true, 12 | } 13 | ); 14 | 15 | this.getDirections = this.getDirections.bind(this); 16 | this.renderDirections = this.renderDirections.bind(this); 17 | this.addMarker = this.addMarker.bind(this); 18 | this.clearDirections = this.clearDirections.bind(this); 19 | } 20 | 21 | addMarker(position) { 22 | const marker = new google.maps.Marker({ 23 | position, 24 | map: this.map, 25 | label: 'A' 26 | }); 27 | 28 | this.pathMarkers.push(marker); 29 | 30 | const markers = this.pathMarkers; 31 | 32 | if (markers.length > 1) { 33 | markers[0].setMap(null); 34 | marker.setMap(null); 35 | this.getDirections(markers); 36 | } 37 | } 38 | 39 | clearDirections() { 40 | this.directionsRenderer.setMap(null); 41 | this.directionsRenderer = new google.maps.DirectionsRenderer( 42 | { 43 | map: this.map, 44 | preserveViewport: true, 45 | } 46 | ); 47 | this.updateState({ 48 | distance: 0, 49 | polyline: '', 50 | start_address: 'N/A', 51 | end_address: 'N/A' 52 | }); 53 | } 54 | 55 | undo() { 56 | let markers = this.pathMarkers; 57 | 58 | if (markers.length > 2) { 59 | this.pathMarkers.pop(); 60 | this.getDirections(this.pathMarkers); 61 | } else if (markers.length === 2) { 62 | let marker = markers[0]; 63 | // this.createMap(this.map.getCenter(), this.map.getZoom()); 64 | this.clearDirections(); 65 | this.pathMarkers = []; 66 | this.addMarker(marker.position); 67 | } else if (markers.length === 1) { 68 | this.clearDirections(); 69 | this.pathMarkers[0].setMap(null); 70 | this.pathMarkers = []; 71 | } 72 | } 73 | 74 | getDirections(markers) { 75 | const waypoints = markers 76 | .slice(1, markers.length - 1) 77 | .map(marker => { 78 | return { 79 | location: marker.position, 80 | stopover: false 81 | }; 82 | }); 83 | 84 | this.directionsService.route( 85 | { 86 | origin: markers[0].position, 87 | destination: markers[markers.length - 1].position, 88 | waypoints: waypoints, 89 | optimizeWaypoints: true, 90 | travelMode: "WALKING" 91 | }, (response) => { 92 | this.renderDirections(response); 93 | } 94 | ); 95 | } 96 | 97 | renderDirections(response) { 98 | const route = response.routes[0]; 99 | const distanceInMiles = (route.legs[0].distance.value * .000622).toFixed(2); 100 | this.updateState({ 101 | distance: distanceInMiles, 102 | polyline: route.overview_polyline, 103 | start_address: route.legs[0].start_address, 104 | end_address: route.legs[0].end_address 105 | }); 106 | 107 | this.directionsRenderer.setDirections(response); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | 5 | // react router 6 | import { Router, Route, IndexRoute, hashHistory, IndexRedirect } from 'react-router'; 7 | 8 | // react components 9 | import App from './app'; 10 | import SessionFormContainer from './session_form/session_form_container'; 11 | import Frontpage from './frontpage/frontpage'; 12 | import Home from './home/home'; 13 | 14 | // paths 15 | import PathsMain from './home/paths/paths_main'; 16 | import PathFormContainer from './home/paths/paths_form/path_form_container'; 17 | import PathIndexContainer from './home/paths/paths_index/path_index_container'; 18 | import PathShowContainer from './home/paths/path_show/path_show_container'; 19 | 20 | // dashboard 21 | import DashboardContainer from './home/dashboard/dashboard_container'; 22 | 23 | // activities 24 | import ActivityContainer from './home/activity_feed/activity_container'; 25 | 26 | // friends 27 | import FriendsTabs from './friends/friends_tab'; 28 | import AllFriendsContainer from './friends/all_friends_container'; 29 | import FriendSearchContainer from './friends/friend_search_container'; 30 | 31 | const Root = ({ store }) => { 32 | const _ensureLoggedIn = (nextState, replace) => { 33 | const currentUser = store.getState().session.currentUser; 34 | if (!currentUser) { 35 | replace('/'); 36 | } 37 | }; 38 | 39 | const _redirectIfLoggedIn = (nextState, replace) => { 40 | const currentUser = store.getState().session.currentUser; 41 | if (currentUser) { 42 | replace('/home'); 43 | } 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 50 | 53 | 54 | 58 | 62 | 63 | 67 | 68 | 69 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | }; 98 | 99 | export default Root; 100 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/paths/path_update_form.scss: -------------------------------------------------------------------------------- 1 | .path-update-overlay { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | background-color: rgba(99, 99, 99, 0.8); 8 | z-index: 3; 9 | 10 | } 11 | 12 | .path-update-content { 13 | box-shadow: 0px 0px 8px 1px rgba(0,0,0,1); 14 | position: absolute; 15 | width: 350px; 16 | height: 450px; 17 | top: 15%; 18 | left: 40%; 19 | right: 50%; 20 | bottom: 50%; 21 | background-color: #fff; 22 | 23 | .path-update-form { 24 | width: 240px; 25 | height: 400px; 26 | margin: auto; 27 | 28 | form { 29 | margin-top: 25px; 30 | 31 | div { 32 | margin: 10px 0; 33 | } 34 | .duration-container { 35 | padding-bottom: 20px; 36 | border-bottom: 1px solid #d1d0cf; 37 | } 38 | } 39 | 40 | h3 { 41 | font-size: 26px; 42 | padding: 25px 0; 43 | border-bottom: 1px solid #d1d0cf; 44 | color: #444444; 45 | font-weight: 400; 46 | letter-spacing: 1px; 47 | text-align: center; 48 | } 49 | 50 | label { 51 | display: block; 52 | font-size: 12px; 53 | color: #8c8c8c; 54 | margin-bottom: 10px; 55 | font-weight: 600; 56 | } 57 | 58 | input[type='date'] { 59 | color: #108cc9; 60 | padding: 3px 0; 61 | font-size: 13px; 62 | transition: .2s; 63 | border: 1px solid #c3c2c2; 64 | border-radius: 3px; 65 | // width: 60px; 66 | &:focus { 67 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 68 | background: #fff; 69 | } 70 | } 71 | 72 | input[type='text'] { 73 | border: 1px solid #c3c2c2; 74 | border-radius: 3px; 75 | font-size: 12px; 76 | padding: 5px; 77 | width: 140px; 78 | height: 20px; 79 | &:focus { 80 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 81 | background: #fff; 82 | } 83 | } 84 | 85 | input[type='tel'] { 86 | margin: 2px; 87 | border: 1px solid #c3c2c2; 88 | padding: 5px; 89 | font-size: 12px; 90 | width: 25px; 91 | border-radius: 3px; 92 | transition: .2s; 93 | &:focus { 94 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 95 | background: #fff; 96 | } 97 | } 98 | 99 | 100 | input[name='hours'] { 101 | margin-left: 0; 102 | } 103 | 104 | .path-update-form-buttons { 105 | display: flex; 106 | align-items: center; 107 | margin-top: 20px;; 108 | // border: 1px solid black; 109 | 110 | input[type='submit'] { 111 | background: -webkit-gradient( 112 | linear, left top, left bottom, 113 | from(#b6b6b6), 114 | to(#a9a6a6)); 115 | text-align: center; 116 | color: #fff; 117 | height: 20px; 118 | cursor: pointer; 119 | text-transform: uppercase; 120 | padding: 5px 10px; 121 | font-size: 11px; 122 | font-weight: 700; 123 | letter-spacing: 1.5px; 124 | border-radius: 5px; 125 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 126 | 127 | &:hover { 128 | background: -webkit-gradient( 129 | linear, left top, left bottom, 130 | from(#878585), 131 | to(#5e5e5e)); 132 | } 133 | } 134 | 135 | p { 136 | font-size: 12px; 137 | font-weight: lighter; 138 | padding: 0 10px; 139 | } 140 | 141 | 142 | .cancel-button { 143 | text-align: center; 144 | font-size: 12px; 145 | color: #0076c0; 146 | cursor: pointer; 147 | } 148 | 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /frontend/components/home/paths/path_show/path_update_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | convertTimeToSeconds, 4 | todaysDate 5 | } from '../../../../util/math_calculations'; 6 | 7 | class PathUpdateForm extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.handleSubmit = this.handleSubmit.bind(this); 12 | this.setDuration = this.setDuration.bind(this); 13 | 14 | this.state = { 15 | description: "", 16 | done_date: "" 17 | }; 18 | 19 | this.hours = 0; 20 | this.minutes = 0; 21 | this.seconds = 0; 22 | } 23 | 24 | componentDidMount () { 25 | this.setState(this.props.path); 26 | } 27 | 28 | handleSubmit(e) { 29 | e.preventDefault(); 30 | const duration = convertTimeToSeconds(this.hours, this.minutes, this.seconds); 31 | const path = Object.assign({}, this.state, { duration }, { done: true }); 32 | this.props.closeModal(); 33 | this.props.updatePath(path); 34 | } 35 | 36 | setDuration(e) { 37 | const name = e.currentTarget.name; 38 | switch(name) { 39 | case 'hours': 40 | this.hours = parseInt(e.currentTarget.value); 41 | break; 42 | case 'minutes': 43 | this.minutes = parseInt(e.currentTarget.value); 44 | break; 45 | case 'seconds': 46 | this.seconds = parseInt(e.currentTarget.value); 47 | break; 48 | } 49 | } 50 | 51 | update(field) { 52 | return (e) => { 53 | this.setState({ 54 | [field]: e.currentTarget.value 55 | }); 56 | }; 57 | } 58 | 59 | render () { 60 | return ( 61 |
    62 |

    {this.props.path.name}

    63 |
    64 |
    65 | 66 | 70 |
    71 |
    72 | 73 | 80 |
    81 |
    82 | 83 | : 91 | : 99 | 107 |
    108 |
    109 | 110 |

    or

    111 |
    this.props.closeModal() 115 | }>Cancel 116 |
    117 |
    118 |
    119 |
    120 | ); 121 | } 122 | 123 | } 124 | 125 | export default PathUpdateForm; 126 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/dashboard/dashboard.scss: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | display: flex; 3 | color: #444444; 4 | 5 | 6 | .dashboard-body { 7 | min-width: 664px; 8 | 9 | header { 10 | margin-top: 8px; 11 | padding-bottom: 27px; 12 | border-bottom: 1px solid #d1d0cf; 13 | 14 | h2 { 15 | text-transform: uppercase; 16 | letter-spacing: 1px; 17 | font-weight: lighter; 18 | margin-left: 5px; 19 | font-weight: lighter; 20 | } 21 | } 22 | 23 | .run-stats { 24 | display: flex; 25 | background: #f7f7f7; 26 | margin-top: 28px; 27 | 28 | span { 29 | text-align: center; 30 | padding: 10px 15px; 31 | border: 1px solid #c3c2c2; 32 | width: 100%; 33 | min-width: 40px; 34 | 35 | &:first-child { 36 | border-right: 0; 37 | } 38 | 39 | &:last-child { 40 | border-left: 0; 41 | } 42 | 43 | h3 { 44 | text-transform: uppercase; 45 | font-size: 14px; 46 | color: #adabab; 47 | font-weight: lighter; 48 | margin-top: 5px; 49 | } 50 | 51 | p { 52 | color: #616060; 53 | font-size: 20px; 54 | padding: 6px 0; 55 | } 56 | 57 | h5 { 58 | text-transform: lowercase; 59 | font-size: 14px; 60 | color: #adabab; 61 | font-weight: lighter; 62 | } 63 | } 64 | } 65 | 66 | h3 { 67 | margin-top: 25px; 68 | padding-bottom: 15px; 69 | border-bottom: 1px solid #d1d0cf; 70 | text-transform: uppercase; 71 | letter-spacing: 1px; 72 | font-weight: lighter; 73 | margin-left: 5px; 74 | font-weight: lighter; 75 | font-size: 22px; 76 | } 77 | 78 | .pending-runs { 79 | 80 | ul { 81 | display: flex; 82 | flex-wrap: wrap; 83 | 84 | li { 85 | position: relative; 86 | margin: 15px 12px; 87 | img { 88 | border: 1px solid #d1d0cf; 89 | display: block; 90 | } 91 | 92 | .map-details { 93 | width: 100px; 94 | background: #fff; 95 | font-size: 13px; 96 | position: absolute; 97 | top: 80px; 98 | left: 5px; 99 | opacity: .9; 100 | padding: 5px 8px; 101 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 102 | 103 | &:hover { 104 | opacity: 1; 105 | color: #2f98d9; 106 | } 107 | 108 | p { 109 | white-space: nowrap; 110 | overflow: hidden; 111 | text-overflow: ellipsis; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | .complete-runs { 119 | 120 | li { 121 | padding: 10px; 122 | border-bottom: 1px solid #d1d0cf; 123 | display: flex; 124 | align-items: center; 125 | margin: 15px 0 10 0; 126 | img { 127 | border: 1px solid #d1d0cf; 128 | display: block; 129 | } 130 | 131 | .duration { 132 | border-left: 1px solid #d1d0cf; 133 | border-right: 1px solid #d1d0cf; 134 | } 135 | 136 | .stats { 137 | min-width: 130px; 138 | padding: 0 20px; 139 | 140 | h4 { 141 | font-weight: lighter; 142 | padding-bottom: 15px; 143 | color: #8e8e8e; 144 | } 145 | p { 146 | font-size: 22px; 147 | font-weight: 100; 148 | color: #696969 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /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 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "MapMyPath_#{Rails.env}" 58 | config.action_mailer.perform_caching = false 59 | 60 | # Ignore bad email addresses and do not raise email delivery errors. 61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 62 | # config.action_mailer.raise_delivery_errors = false 63 | 64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 65 | # the I18n.default_locale when a translation cannot be found). 66 | config.i18n.fallbacks = true 67 | 68 | # Send deprecation notices to registered listeners. 69 | config.active_support.deprecation = :notify 70 | 71 | # Use default logging formatter so that PID and timestamp are not suppressed. 72 | config.log_formatter = ::Logger::Formatter.new 73 | 74 | # Use a different logger for distributed setups. 75 | # require 'syslog/logger' 76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 77 | 78 | if ENV["RAILS_LOG_TO_STDOUT"].present? 79 | logger = ActiveSupport::Logger.new(STDOUT) 80 | logger.formatter = config.log_formatter 81 | config.logger = ActiveSupport::TaggedLogging.new(logger) 82 | end 83 | 84 | # Do not dump schema after migrations. 85 | config.active_record.dump_schema_after_migration = false 86 | end 87 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20170428052457) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "comments", force: :cascade do |t| 19 | t.integer "author_id", null: false 20 | t.integer "path_id", null: false 21 | t.text "body", null: false 22 | t.datetime "created_at", null: false 23 | t.datetime "updated_at", null: false 24 | end 25 | 26 | create_table "friends", force: :cascade do |t| 27 | t.integer "user_one", null: false 28 | t.integer "user_two", null: false 29 | t.datetime "created_at", null: false 30 | t.datetime "updated_at", null: false 31 | end 32 | 33 | create_table "paths", force: :cascade do |t| 34 | t.string "name", null: false 35 | t.text "polyline", null: false 36 | t.float "distance", null: false 37 | t.string "start_address", null: false 38 | t.string "end_address", null: false 39 | t.integer "duration", default: 0 40 | t.boolean "done", default: false 41 | t.date "done_date" 42 | t.integer "user_id", null: false 43 | t.datetime "created_at", null: false 44 | t.datetime "updated_at", null: false 45 | t.text "description", default: "" 46 | t.index ["user_id"], name: "index_paths_on_user_id", using: :btree 47 | end 48 | 49 | create_table "relationships", force: :cascade do |t| 50 | t.integer "user_one_id", null: false 51 | t.integer "user_two_id", null: false 52 | t.integer "status", null: false 53 | t.integer "action_user_id", null: false 54 | t.datetime "created_at", null: false 55 | t.datetime "updated_at", null: false 56 | t.index ["user_one_id", "user_two_id"], name: "index_relationships_on_user_one_id_and_user_two_id", using: :btree 57 | end 58 | 59 | create_table "users", force: :cascade do |t| 60 | t.string "email", null: false 61 | t.string "first_name", null: false 62 | t.string "last_name", null: false 63 | t.string "password_digest", null: false 64 | t.string "session_token", null: false 65 | t.string "img_url", default: "https://s3.us-east-2.amazonaws.com/mapmyrun-dev/default_avatar.png" 66 | t.datetime "created_at", null: false 67 | t.datetime "updated_at", null: false 68 | t.index ["email"], name: "index_users_on_email", unique: true, using: :btree 69 | t.index ["first_name"], name: "index_users_on_first_name", using: :btree 70 | t.index ["last_name"], name: "index_users_on_last_name", using: :btree 71 | t.index ["session_token"], name: "index_users_on_session_token", unique: true, using: :btree 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /frontend/components/home/paths/paths_form/map.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MapManager from './map_manager'; 3 | import Modal from 'react-modal'; 4 | 5 | class Map extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | modalIsOpen: true 11 | }; 12 | 13 | this.createMap = this.createMap.bind(this); 14 | this.clearMap = this.clearMap.bind(this); 15 | this.undoMap = this.undoMap.bind(this); 16 | } 17 | 18 | componentDidMount () { 19 | const initialPosition = { lat: 40.745209, lng: -73.993957 }; 20 | 21 | navigator.geolocation.getCurrentPosition( 22 | (position) => { 23 | this.createMap( 24 | { lat: position.coords.latitude, lng: position.coords.longitude } 25 | ); 26 | }, (error) => { 27 | this.createMap(initialPosition); 28 | } 29 | ); 30 | } 31 | 32 | createMap (initialPosition, zoom) { 33 | zoom = zoom || 15; 34 | 35 | this.map = new google.maps.Map(this.refs.map, { 36 | center: initialPosition, 37 | zoom, 38 | rotateControl: true, 39 | mapTypeControlOptions: 40 | { 41 | style: google.maps.MapTypeControlStyle.DROPDOWN_MENU 42 | }, 43 | draggableCursor: 'crosshair', 44 | styles: [ 45 | { 46 | "featureType": "administrative", 47 | "stylers": [ 48 | { 49 | "visibility": "on" 50 | } 51 | ] 52 | }, 53 | { 54 | "featureType": "landscape.man_made", 55 | "stylers": [ 56 | { 57 | "visibility": "on" 58 | } 59 | ] 60 | }, 61 | { 62 | "featureType": "landscape.natural", 63 | "stylers": [ 64 | { 65 | "visibility": "on" 66 | } 67 | ] 68 | }, 69 | { 70 | "featureType": "poi", 71 | "stylers": [ 72 | { 73 | "saturation": -35 74 | }, 75 | { 76 | "lightness": -15 77 | }, 78 | { 79 | "visibility": "off" 80 | } 81 | ] 82 | }, 83 | { 84 | "featureType": "poi.medical", 85 | "stylers": [ 86 | { 87 | "visibility": "on" 88 | } 89 | ] 90 | }, 91 | { 92 | "featureType": "poi.park", 93 | "stylers": [ 94 | { 95 | "visibility": "simplified" 96 | } 97 | ] 98 | }, 99 | { 100 | "featureType": "poi.school", 101 | "stylers": [ 102 | { 103 | "visibility": "on" 104 | } 105 | ] 106 | }, 107 | { 108 | "featureType": "transit", 109 | "stylers": [ 110 | { 111 | "visibility": "off" 112 | } 113 | ] 114 | }, 115 | { 116 | "featureType": "water", 117 | "stylers": [ 118 | { 119 | "visibility": "on" 120 | } 121 | ] 122 | } 123 | ] 124 | }); 125 | 126 | this.MapManager = new MapManager(this.map, this.props.setState); 127 | 128 | this.map.addListener('click', (e) => { 129 | this.MapManager.addMarker(e.latLng); 130 | }); 131 | 132 | this.map.addListener('idle', (e) => { 133 | this.setState({ modalIsOpen: false }); 134 | }); 135 | } 136 | 137 | clearMap (e) { 138 | e.preventDefault(); 139 | 140 | if (this.MapManager.pathMarkers.length > 0) { 141 | this.MapManager.clearDirections(); 142 | this.MapManager.pathMarkers[0].setMap(null); 143 | this.MapManager.pathMarkers = []; 144 | } 145 | } 146 | 147 | undoMap (e) { 148 | e.preventDefault(); 149 | 150 | this.MapManager.undo(); 151 | } 152 | 153 | render () { 154 | return ( 155 |
    156 | 162 |
    163 |

    Loading mapping tools

    164 |
    165 |

    Please wait

    166 |
    167 |
    168 | 169 |
    170 | 171 | 172 |
    173 |
    174 |
    175 |
    176 | ); 177 | } 178 | } 179 | 180 | export default Map; 181 | -------------------------------------------------------------------------------- /app/assets/stylesheets/frontpage/frontpage.scss: -------------------------------------------------------------------------------- 1 | .front-page { 2 | 3 | .frontpage-content { 4 | display: flex; 5 | justify-content: flex-end; 6 | min-width: 930px; 7 | background: image-url("frontpage-min.jpg"); 8 | background-size: cover; 9 | padding: 55px; 10 | background-position-y: -164px; 11 | background-repeat: no-repeat; 12 | 13 | section { 14 | width: 490px; 15 | height: 380px; 16 | display: flex; 17 | flex-direction: column; 18 | position: relative; 19 | right: 15%; 20 | color: white; 21 | text-shadow: 1px 1px 7px rgba(150, 150, 150, 0.94); 22 | 23 | h1 { 24 | text-transform: uppercase; 25 | letter-spacing: .5px; 26 | font-size: 36px; 27 | line-height: 135%; 28 | font-weight: 600; 29 | 30 | } 31 | 32 | h3 { 33 | font-size: 15px; 34 | font-weight: 400; 35 | text-shadow: 2px 1px 9px rgba(150, 150, 150, 0.94); 36 | margin: 10px 0 0 5px; 37 | } 38 | 39 | .login-signup { 40 | display: flex; 41 | margin: 20px 0 0 0; 42 | font-family: sans-serif; 43 | 44 | .or { 45 | font-size: 15px; 46 | padding: 0 15px; 47 | color: white; 48 | text-shadow: 2px 1px 9px rgba(150, 150, 150, 0.94); 49 | display: flex; 50 | align-items: center; 51 | font-weight: lighter; 52 | } 53 | 54 | .guest-login, .signup-button { 55 | display: flex; 56 | align-items: center; 57 | color: #fff; 58 | height: 20px; 59 | cursor: pointer; 60 | transition-duration: .4s; 61 | text-transform: uppercase; 62 | padding: 10px 20px; 63 | font-size: 14px; 64 | font-weight: 700; 65 | letter-spacing: 1.5px; 66 | border-radius: 10px; 67 | box-shadow: -4px 6px 31px -8px rgba(0,0,0,0.75) 68 | } 69 | 70 | .guest-login { 71 | background: -webkit-gradient( 72 | linear, left top, left bottom, 73 | from(#4499db), 74 | to(#177ed7)); 75 | &:hover { 76 | background: -webkit-gradient( 77 | linear, left top, left bottom, 78 | from(#3f92c6), 79 | to(#0074bc)) 80 | } 81 | } 82 | 83 | .signup-button { 84 | background: -webkit-gradient( 85 | linear, left top, left bottom, 86 | from(#ff9628), 87 | to(#ff8714)); 88 | &:hover { 89 | background: -webkit-gradient( 90 | linear, left top, left bottom, 91 | from(#f08a15), 92 | to(#e56e06)) 93 | } 94 | } 95 | } 96 | 97 | .login { 98 | display: flex; 99 | justify-content: flex-end; 100 | position: relative; 101 | top: 180px; 102 | align-items: center; 103 | margin-right: 25px; 104 | 105 | 106 | p { 107 | font-weight: 600; 108 | font-size: 15px; 109 | letter-spacing: .5px; 110 | margin: 0 8px; 111 | } 112 | 113 | .login-button { 114 | display: flex; 115 | align-items: center; 116 | // justify-content: center; 117 | color: #fff; 118 | height: 14px; 119 | cursor: pointer; 120 | transition-duration: .4s; 121 | text-transform: uppercase; 122 | padding: 8px 20px; 123 | font-size: 12px; 124 | font-weight: 700; 125 | letter-spacing: .8px; 126 | border-radius: 5px; 127 | box-shadow: -4px 6px 31px -8px rgba(0,0,0,0.75); 128 | 129 | background: -webkit-gradient( 130 | linear, left top, left bottom, 131 | from(#5b5b5b), 132 | to(#454545)); 133 | 134 | &:hover { 135 | background: -webkit-gradient( 136 | linear, left top, left bottom, 137 | from(#4f4f4f), 138 | to(#222222)) 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | .footer { 146 | display: flex; 147 | justify-content: center; 148 | background: #f9f7f6; 149 | width: 100%; 150 | 151 | // * { 152 | // border: 1px solid black; 153 | // } 154 | 155 | .features { 156 | display: flex; 157 | // width: 800px; 158 | margin: 30px 0 40px 0; 159 | color: #999; 160 | 161 | &>div { 162 | display: flex; 163 | flex-direction: column; 164 | align-items: center; 165 | width: 220px; 166 | margin: 0 28px; 167 | 168 | .fa { 169 | color: #67b1ea; 170 | margin-bottom: 25px; 171 | } 172 | 173 | h3 { 174 | text-transform: uppercase; 175 | letter-spacing: .5px; 176 | font-weight: 800; 177 | } 178 | 179 | p { 180 | text-align: center; 181 | font-size: 13px; 182 | } 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage/paths/path_form.scss: -------------------------------------------------------------------------------- 1 | .create-path { 2 | // color: #444444; 3 | 4 | header { 5 | display: flex; 6 | text-transform: uppercase; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding-bottom: 20px; 10 | border-bottom: 1px solid #d1d0cf; 11 | letter-spacing: 1px; 12 | 13 | .back-button { 14 | font-size: 12px; 15 | color: white; 16 | padding: 15px 18px; 17 | transition: .6s; 18 | border-radius: 2px; 19 | font-weight: bold; 20 | background: -webkit-gradient( 21 | linear, left top, left bottom, 22 | from(#4499db), 23 | to(#177ed7)); 24 | &:hover { 25 | background: -webkit-gradient( 26 | linear, left top, left bottom, 27 | from(#3f92c6), 28 | to(#0074bc)) 29 | } 30 | } 31 | .path-form-header { 32 | margin-left: 5px; 33 | font-weight: lighter; 34 | } 35 | } 36 | } 37 | 38 | .path-main { 39 | color: #444444; 40 | display: flex; 41 | margin-top: 20px; 42 | 43 | .errors { 44 | font-size: 11px; 45 | color: rgba(255, 0, 0, 0.69); 46 | width: 180px; 47 | } 48 | 49 | .path-form { 50 | 51 | .path-details { 52 | padding: 10px 0px 0 0; 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | 57 | h3 { 58 | text-transform: uppercase; 59 | margin: 8px 0; 60 | padding-bottom: 4px; 61 | border-bottom: 1px solid gray; 62 | font-size: 18px; 63 | } 64 | 65 | 66 | li { 67 | margin: 5px; 68 | 69 | &:last-child { 70 | padding-bottom: 5px; 71 | border-bottom: 1px solid gray; 72 | } 73 | } 74 | 75 | p { 76 | width: 240px; 77 | vertical-align: middle; 78 | } 79 | 80 | .address { 81 | height: 38px; 82 | } 83 | 84 | h4 { 85 | margin: 8px 0; 86 | font-size: 14px; 87 | text-align: center; 88 | } 89 | 90 | p { 91 | text-align: center; 92 | font-size: 14px; 93 | } 94 | } 95 | 96 | .path-form-input { 97 | padding: 10px 0px 0 0; 98 | display: flex; 99 | flex-direction: column; 100 | align-items: center; 101 | 102 | 103 | h3 { 104 | text-transform: uppercase; 105 | margin: 8px 0; 106 | padding-bottom: 4px; 107 | border-bottom: 1px solid gray; 108 | font-size: 18px; 109 | } 110 | 111 | .name-input, .description-input { 112 | margin: 5px 0; 113 | border: 1px solid #c8c7cc; 114 | padding: 10px; 115 | font-size: 12px; 116 | width: 180px; 117 | border-radius: 3px; 118 | transition: .2s; 119 | &:focus { 120 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 121 | background: #fff; 122 | } 123 | } 124 | 125 | .select-container { 126 | border: 1px solid #c8c7cc; 127 | border-radius: 2px; 128 | width: 200px; 129 | margin: 5px 0; 130 | 131 | select { 132 | background: transparent; 133 | border: none; 134 | font-size: 12px; 135 | height: 35px; 136 | padding: 5px; 137 | width: 200px; 138 | // transition: .2s; 139 | &:focus { 140 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 141 | background: #fff; 142 | } 143 | } 144 | } 145 | 146 | input[type='date'] { 147 | width: 100%; 148 | padding: 8px 15px; 149 | font-size: 12px; 150 | text-transform: uppercase; 151 | transition: .2s; 152 | &:focus { 153 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 154 | background: #fff; 155 | } 156 | } 157 | 158 | #date-container { 159 | border: 1px solid #c8c7cc; 160 | border-radius: 3px; 161 | width: 200px; 162 | margin: 5px 0; 163 | } 164 | 165 | #duration-container { 166 | width: 200px; 167 | justify-content: space-between; 168 | align-items: center; 169 | 170 | input[type='tel'] { 171 | margin: 5px 0; 172 | border: 1px solid #c8c7cc; 173 | padding: 10px; 174 | font-size: 12px; 175 | width: 38px; 176 | border-radius: 3px; 177 | transition: .2s; 178 | &:focus { 179 | box-shadow: 0px 0px 4px 1px rgba(64,128,255,1); 180 | background: #fff; 181 | } 182 | } 183 | } 184 | 185 | input[type='submit'] { 186 | background: -webkit-gradient( 187 | linear, left top, left bottom, 188 | from(#ff9628), 189 | to(#ff8714)); 190 | width: 160px; 191 | text-align: center; 192 | color: #fff; 193 | height: 20px; 194 | cursor: pointer; 195 | text-transform: uppercase; 196 | padding: 5px 20px; 197 | font-size: 13px; 198 | font-weight: 700; 199 | letter-spacing: 1.5px; 200 | border-radius: 5px; 201 | margin-top: 10px; 202 | 203 | &:hover { 204 | background: -webkit-gradient( 205 | linear, left top, left bottom, 206 | from(#f08a15), 207 | to(#e56e06)) 208 | } 209 | } 210 | 211 | .false { 212 | display: none; 213 | } 214 | 215 | .true { 216 | display: flex; 217 | } 218 | } 219 | 220 | } 221 | } 222 | --------------------------------------------------------------------------------