diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..921bf57ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile ~/.gitignore_global + +# Ignore bundler config +/.bundle + +# Ignore Mac datastore files +.DS_Store + +# Ignore chrome driver log +chromedriver.log + +# Ignore simplecov reports +coverage + +# Ignore rails_best_practices report +rails_best_practices_output.html + +# Ignore brakeman report +brakeman.html + +# Ignore the default SQLite database. +/db/*.sqlite3 + +# Ignore all logfiles and tempfiles. +/log/*.log +/tmp + +# Ignore file uploads +/public/uploads + +# Ignore sensitive data +/config/settings/local.rb + +# Ignore Redis' dataset snapshot +dump.rdb \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..5902dd3af --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--colour --drb diff --git a/.slugignore b/.slugignore new file mode 100644 index 000000000..99c94112c --- /dev/null +++ b/.slugignore @@ -0,0 +1,2 @@ +features +spec \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..db2ff5111 --- /dev/null +++ b/Gemfile @@ -0,0 +1,63 @@ +source 'https://rubygems.org' + +ruby '1.9.3' +gem 'rails', '3.2.7' + +gem 'unicorn' + +gem 'mongoid', '< 3.0.0.rc' # NOTE mongoind dependant gems are not ready for mongodb 3.0 +gem 'mongoid_geospatial', git: 'git://github.com/kristianmandrup/mongoid_geospatial.git' +gem 'bson', '1.6.2' +gem 'bson_ext', '1.6.2' +gem 'thin' +gem 'haml' +gem 'therubyracer' +gem 'omniauth-facebook' +gem "koala" +gem 'validates_timeliness' +gem 'rest-client' +gem 'nokogiri' +gem 'http_accept_language' +gem 'jquery-rails' +gem 'client_side_validations' +#gem 'wicked' +gem 'resque', require: 'resque/server' +gem 'kaminari' +gem 'rabl' +gem 'simpleconfig' + +group :development, :test do + gem "bullet" + gem 'debugger' + gem 'pry' + gem 'pry-rails' + gem 'factory_girl_rails' + gem 'faker' + gem 'delorean' +end + +group :test do + gem 'rspec-rails', '~> 2.6' + gem "mongoid-rspec", '~> 1.4.5' # NOTE this gem was updated to mongdb 3.0 starting from version 1.4.6 + gem 'cucumber-rails', require: false + gem 'email_spec' + gem 'action_mailer_cache_delivery' + gem 'spork', '~> 1.0rc' + gem 'database_cleaner' + gem 'launchy' + gem 'simplecov', require: false + gem 'webmock', require: false +end + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'less-rails' + gem 'sass-rails' + gem 'coffee-rails' + gem 'uglifier' + gem 'twitter-bootstrap-rails' + gem 'compass-rails' + gem 'haml_assets' + gem 'handlebars_assets' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..31a525a1b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,363 @@ +GIT + remote: git://github.com/kristianmandrup/mongoid_geospatial.git + revision: 5b4122cc0935f6a0f4882a11802bcc6a74076ee4 + specs: + mongoid_geospatial (1.0.1) + activesupport (~> 3.0) + mongoid (>= 2.0.0) + rgeo (>= 0.3.5) + +GEM + remote: https://rubygems.org/ + specs: + action_mailer_cache_delivery (0.3.2) + actionmailer (~> 3.0) + actionmailer (3.2.7) + actionpack (= 3.2.7) + mail (~> 2.4.4) + actionpack (3.2.7) + activemodel (= 3.2.7) + activesupport (= 3.2.7) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.4) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.3) + activemodel (3.2.7) + activesupport (= 3.2.7) + builder (~> 3.0.0) + activerecord (3.2.7) + activemodel (= 3.2.7) + activesupport (= 3.2.7) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activeresource (3.2.7) + activemodel (= 3.2.7) + activesupport (= 3.2.7) + activesupport (3.2.7) + i18n (~> 0.6) + multi_json (~> 1.0) + addressable (2.3.2) + arel (3.0.2) + bson (1.6.2) + bson_ext (1.6.2) + bson (~> 1.6.2) + builder (3.0.0) + bullet (4.1.6) + uniform_notifier (~> 1.0.0) + capybara (1.1.2) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (~> 2.0) + xpath (~> 0.1.4) + childprocess (0.3.5) + ffi (~> 1.0, >= 1.0.6) + chronic (0.7.0) + chunky_png (1.2.6) + client_side_validations (3.1.4) + coderay (1.0.7) + coffee-rails (3.2.2) + coffee-script (>= 2.2.0) + railties (~> 3.2.0) + coffee-script (2.2.0) + coffee-script-source + execjs + coffee-script-source (1.3.3) + columnize (0.3.6) + commonjs (0.2.6) + compass (0.12.2) + chunky_png (~> 1.2) + fssm (>= 0.2.7) + sass (~> 3.1) + compass-rails (1.0.3) + compass (>= 0.12.2, < 0.14) + crack (0.3.1) + cucumber (1.2.1) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.11.0) + json (>= 1.4.6) + cucumber-rails (1.3.0) + capybara (>= 1.1.2) + cucumber (>= 1.1.8) + nokogiri (>= 1.5.0) + daemons (1.1.9) + database_cleaner (0.8.0) + debugger (1.2.0) + columnize (>= 0.3.1) + debugger-linecache (~> 1.1.1) + debugger-ruby_core_source (~> 1.1.3) + debugger-linecache (1.1.2) + debugger-ruby_core_source (>= 1.1.1) + debugger-ruby_core_source (1.1.3) + delorean (2.0.0) + chronic + diff-lcs (1.1.3) + email_spec (1.2.1) + mail (~> 2.2) + rspec (~> 2.0) + erubis (2.7.0) + eventmachine (0.12.10) + execjs (1.4.0) + multi_json (~> 1.0) + factory_girl (4.0.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.0.0) + factory_girl (~> 4.0.0) + railties (>= 3.0.0) + faker (1.0.1) + i18n (~> 0.4) + faraday (0.8.4) + multipart-post (~> 1.1) + ffi (1.1.5) + fssm (0.2.9) + gherkin (2.11.2) + json (>= 1.4.6) + haml (3.1.7) + haml_assets (0.1.0) + haml + tilt + handlebars_assets (0.6.4) + execjs (>= 1.2.9) + sprockets (>= 2.0.3) + tilt + hashie (1.2.0) + hike (1.2.1) + http_accept_language (1.0.2) + httpauth (0.1) + i18n (0.6.1) + journey (1.0.4) + jquery-rails (2.1.1) + railties (>= 3.1.0, < 5.0) + thor (~> 0.14) + json (1.7.5) + jwt (0.1.5) + multi_json (>= 1.0) + kaminari (0.14.0) + actionpack (>= 3.0.0) + activesupport (>= 3.0.0) + kgio (2.7.4) + koala (1.5.0) + faraday (~> 0.7) + multi_json (~> 1.3) + launchy (2.1.2) + addressable (~> 2.3) + less (2.2.2) + commonjs (~> 0.2.6) + less-rails (2.2.3) + actionpack (>= 3.1) + less (~> 2.2.0) + libv8 (3.3.10.4) + libwebsocket (0.1.5) + addressable + mail (2.4.4) + i18n (>= 0.4.0) + mime-types (~> 1.16) + treetop (~> 1.4.8) + method_source (0.8) + mime-types (1.19) + mongo (1.6.2) + bson (~> 1.6.2) + mongoid (2.4.12) + activemodel (~> 3.1) + mongo (<= 1.6.2) + tzinfo (~> 0.3.22) + mongoid-rspec (1.4.5) + mongoid (>= 2.4.6) + rake + rspec (>= 2.9) + multi_json (1.3.6) + multipart-post (1.1.5) + nokogiri (1.5.5) + oauth2 (0.8.0) + faraday (~> 0.8) + httpauth (~> 0.1) + jwt (~> 0.1.4) + multi_json (~> 1.0) + rack (~> 1.2) + omniauth (1.1.1) + hashie (~> 1.2) + rack + omniauth-facebook (1.4.1) + omniauth-oauth2 (~> 1.1.0) + omniauth-oauth2 (1.1.0) + oauth2 (~> 0.8.0) + omniauth (~> 1.0) + polyglot (0.3.3) + pry (0.9.10) + coderay (~> 1.0.5) + method_source (~> 0.8) + slop (~> 3.3.1) + pry-rails (0.2.1) + pry (>= 0.9.10) + rabl (0.7.1) + activesupport (>= 2.3.14) + multi_json (~> 1.0) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-protection (1.2.0) + rack + rack-ssl (1.3.2) + rack + rack-test (0.6.1) + rack (>= 1.0) + rails (3.2.7) + actionmailer (= 3.2.7) + actionpack (= 3.2.7) + activerecord (= 3.2.7) + activeresource (= 3.2.7) + activesupport (= 3.2.7) + bundler (~> 1.0) + railties (= 3.2.7) + railties (3.2.7) + actionpack (= 3.2.7) + activesupport (= 3.2.7) + rack-ssl (~> 1.3.2) + rake (>= 0.8.7) + rdoc (~> 3.4) + thor (>= 0.14.6, < 2.0) + raindrops (0.10.0) + rake (0.9.2.2) + rdoc (3.12) + json (~> 1.4) + redis (3.0.1) + redis-namespace (1.2.1) + redis (~> 3.0.0) + resque (1.22.0) + multi_json (~> 1.0) + redis-namespace (~> 1.0) + sinatra (>= 0.9.2) + vegas (~> 0.1.2) + rest-client (1.6.7) + mime-types (>= 1.16) + rgeo (0.3.17) + rspec (2.11.0) + rspec-core (~> 2.11.0) + rspec-expectations (~> 2.11.0) + rspec-mocks (~> 2.11.0) + rspec-core (2.11.1) + rspec-expectations (2.11.2) + diff-lcs (~> 1.1.3) + rspec-mocks (2.11.2) + rspec-rails (2.11.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec (~> 2.11.0) + rubyzip (0.9.9) + sass (3.2.1) + sass-rails (3.2.5) + railties (~> 3.2.0) + sass (>= 3.1.10) + tilt (~> 1.3) + selenium-webdriver (2.25.0) + childprocess (>= 0.2.5) + libwebsocket (~> 0.1.3) + multi_json (~> 1.0) + rubyzip + simpleconfig (2.0.0) + simplecov (0.6.4) + multi_json (~> 1.0) + simplecov-html (~> 0.5.3) + simplecov-html (0.5.3) + sinatra (1.3.3) + rack (~> 1.3, >= 1.3.6) + rack-protection (~> 1.2) + tilt (~> 1.3, >= 1.3.3) + slop (3.3.3) + spork (1.0.0rc3) + sprockets (2.1.3) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + therubyracer (0.10.2) + libv8 (~> 3.3.10) + thin (1.4.1) + daemons (>= 1.0.9) + eventmachine (>= 0.12.6) + rack (>= 1.0.0) + thor (0.16.0) + tilt (1.3.3) + timeliness (0.3.6) + treetop (1.4.10) + polyglot + polyglot (>= 0.3.1) + twitter-bootstrap-rails (2.1.3) + actionpack (>= 3.1) + less-rails (~> 2.2.3) + railties (>= 3.1) + therubyracer (~> 0.10.2) + tzinfo (0.3.33) + uglifier (1.3.0) + execjs (>= 0.3.0) + multi_json (~> 1.0, >= 1.0.2) + unicorn (4.3.1) + kgio (~> 2.6) + rack + raindrops (~> 0.7) + uniform_notifier (1.0.2) + validates_timeliness (3.0.14) + timeliness (~> 0.3.6) + vegas (0.1.11) + rack (>= 1.0.0) + webmock (1.8.9) + addressable (>= 2.2.7) + crack (>= 0.1.7) + xpath (0.1.4) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + action_mailer_cache_delivery + bson (= 1.6.2) + bson_ext (= 1.6.2) + bullet + client_side_validations + coffee-rails + compass-rails + cucumber-rails + database_cleaner + debugger + delorean + email_spec + factory_girl_rails + faker + haml + haml_assets + handlebars_assets + http_accept_language + jquery-rails + kaminari + koala + launchy + less-rails + mongoid (< 3.0.0.rc) + mongoid-rspec (~> 1.4.5) + mongoid_geospatial! + nokogiri + omniauth-facebook + pry + pry-rails + rabl + rails (= 3.2.7) + resque + rest-client + rspec-rails (~> 2.6) + sass-rails + simpleconfig + simplecov + spork (~> 1.0rc) + therubyracer + thin + twitter-bootstrap-rails + uglifier + unicorn + validates_timeliness + webmock diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..467e48d60 --- /dev/null +++ b/LICENSE @@ -0,0 +1,65 @@ +Copyright (c) 2012, diowa +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +client_side_validations is licensed under the MIT License + +FontAwesome is licensed under CC BY 3.0 License + +haml is licensed under the MIT License + +http_accept_language is licensed under the MIT License + +jquery-rails is licensed under the MIT License + +jquery is licensed under the MIT License + +jQuery throttle / debounce is licensed under the MIT License + +kaminari is licensed under the MIT License + +koala is licensed under the MIT License + +mongoid is licensed under the MIT License + +#mongoid_geospatial: no license file provided + +#mongoid_slug is licensed under the MIT License + +nokogiri is licensed under the MIT License + +omniauth-facebook is licensed under the MIT License + +resque is licensed under the MIT License + +rabl is licensed under the MIT License + +rest-client is licensed under the MIT License + +simpleconfig is licensed under the MIT License + +Twitter Bootstrap is licensed under the Apache License, Version 2.0 + +unicorn is licensed under the GPLv2 License + +validates_timeliness is licensed under the MIT License diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..49e7b8f3e --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb +worker: bundle exec rake resque:work QUEUE=* diff --git a/README.md b/README.md new file mode 100644 index 000000000..684990256 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +icare +============== + +**icare** is an open source [carpooling](http://en.wikipedia.org/wiki/Carpool) platform. + + +Bug tracker +----------- + +Have a bug? Please create an issue here on GitHub! Also, when filing please make sure you're familiar with [necolas's guidelines](https://github.com/necolas/issue-guidelines). Thanks! + +(/issues) + + +Internationalization (i18n) +--------------------------- + +TODO + + +Contributing +------------ + +Please make all pull requests against develop branch. Also, if your unit test contains new features, you must include relevant unit tests. Thanks! + +If you are interested in features development, we have priorities. Check out our [To Do](/diowa/icare/wiki/To-Do) list + + +Authors +------- + +**Geremia Taglialatela** + ++ http://github.com/tagliala ++ http://twitter.com/gtagliala + +**Cesidio Di Landa** + ++ http://github.com/cesidio ++ http://twitter.com/cesid + + +Roadmap +-------- + +TODO + +Copyright and license +--------------------- + +**icare** is licensed under the BSD 2-Clause License + +Check the LICENSE file for more information + + +Thanks +------ + +Special thanks to [Mark James](http://www.famfamfam.com/) for [FAMFAMFAM flag icons](http://www.famfamfam.com/lab/icons/flags/) + + +Donations +--------- + +If you like this project or you are considering to use it (or any part of it) for commercial purposes, please make +a donation to the authors. diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..a6dc9dd7e --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +#!/usr/bin/env rake +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +require File.join(Rails.root.to_s, "config", "configuration.rb") + +Icare::Application.load_tasks diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..ceab6e11e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1 \ No newline at end of file diff --git a/app/assets/images/ajax-spinner-16x11.gif b/app/assets/images/ajax-spinner-16x11.gif new file mode 100644 index 000000000..b91c68981 Binary files /dev/null and b/app/assets/images/ajax-spinner-16x11.gif differ diff --git a/app/assets/images/ajax-spinner-24x17.gif b/app/assets/images/ajax-spinner-24x17.gif new file mode 100644 index 000000000..362b532bc Binary files /dev/null and b/app/assets/images/ajax-spinner-24x17.gif differ diff --git a/app/assets/images/ajax-spinner-32x32.gif b/app/assets/images/ajax-spinner-32x32.gif new file mode 100644 index 000000000..0a3a9895f Binary files /dev/null and b/app/assets/images/ajax-spinner-32x32.gif differ diff --git a/app/assets/images/dash_dashboard.jpg b/app/assets/images/dash_dashboard.jpg new file mode 100644 index 000000000..8b6611ad6 Binary files /dev/null and b/app/assets/images/dash_dashboard.jpg differ diff --git a/app/assets/images/dash_road.jpg b/app/assets/images/dash_road.jpg new file mode 100644 index 000000000..d9b2a354f Binary files /dev/null and b/app/assets/images/dash_road.jpg differ diff --git a/app/assets/images/diowa.svg b/app/assets/images/diowa.svg new file mode 100644 index 000000000..c013280d2 --- /dev/null +++ b/app/assets/images/diowa.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico new file mode 100644 index 000000000..d8027100b Binary files /dev/null and b/app/assets/images/favicon.ico differ diff --git a/app/assets/images/flags.png b/app/assets/images/flags.png new file mode 100644 index 000000000..0460bf50d Binary files /dev/null and b/app/assets/images/flags.png differ diff --git a/app/assets/images/markers-s93f4f96d2e.png b/app/assets/images/markers-s93f4f96d2e.png new file mode 100644 index 000000000..454ff5339 Binary files /dev/null and b/app/assets/images/markers-s93f4f96d2e.png differ diff --git a/app/assets/images/markers/coffee.png b/app/assets/images/markers/coffee.png new file mode 100644 index 000000000..478350846 Binary files /dev/null and b/app/assets/images/markers/coffee.png differ diff --git a/app/assets/images/markers/home.png b/app/assets/images/markers/home.png new file mode 100644 index 000000000..65e18e50d Binary files /dev/null and b/app/assets/images/markers/home.png differ diff --git a/app/assets/images/markers/lodging_0star.png b/app/assets/images/markers/lodging_0star.png new file mode 100644 index 000000000..8099d4e14 Binary files /dev/null and b/app/assets/images/markers/lodging_0star.png differ diff --git a/app/assets/images/markers/music-classical.png b/app/assets/images/markers/music-classical.png new file mode 100644 index 000000000..7de1d4ef7 Binary files /dev/null and b/app/assets/images/markers/music-classical.png differ diff --git a/app/assets/images/markers/restaurant.png b/app/assets/images/markers/restaurant.png new file mode 100644 index 000000000..d48df073d Binary files /dev/null and b/app/assets/images/markers/restaurant.png differ diff --git a/app/assets/images/markers/star-3.png b/app/assets/images/markers/star-3.png new file mode 100644 index 000000000..8f0f87389 Binary files /dev/null and b/app/assets/images/markers/star-3.png differ diff --git a/app/assets/images/road.jpeg b/app/assets/images/road.jpeg new file mode 100644 index 000000000..2e7bdcb70 Binary files /dev/null and b/app/assets/images/road.jpeg differ diff --git a/app/assets/images/road.jpg b/app/assets/images/road.jpg new file mode 100644 index 000000000..d773d5aea Binary files /dev/null and b/app/assets/images/road.jpg differ diff --git a/app/assets/images/turtle.svg b/app/assets/images/turtle.svg new file mode 100644 index 000000000..20c5608cf --- /dev/null +++ b/app/assets/images/turtle.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/turtle_black.svg b/app/assets/images/turtle_black.svg new file mode 100644 index 000000000..8d7d1b686 --- /dev/null +++ b/app/assets/images/turtle_black.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee new file mode 100644 index 000000000..ae146f82c --- /dev/null +++ b/app/assets/javascripts/application.js.coffee @@ -0,0 +1,17 @@ +#= require jquery +#= require jquery_ujs +#= require jquery.ba-throttle-debounce + +#= require rails.validations + +#= require twitter/bootstrap + +#= require handlebars + +#= require_tree . + +"use strict" + +String::capitalize = -> + this.replace /(?:^|\s)\S/g, (c) -> + c.toUpperCase() diff --git a/app/assets/javascripts/fbjssdk.js.coffee.erb b/app/assets/javascripts/fbjssdk.js.coffee.erb new file mode 100644 index 000000000..57bba4b3a --- /dev/null +++ b/app/assets/javascripts/fbjssdk.js.coffee.erb @@ -0,0 +1,27 @@ +###global FB:false### + +"use strict" + +window.fbAsyncInit = -> + FB.init + appId : '<%= defined?(APP_CONFIG) ? APP_CONFIG.facebook.app_id : ENV["FACEBOOK_APP_ID"] %>' # App ID + channelUrl : '/fbjssdk_channel' # Channel File + status : true # check login status + cookie : true # enable cookies to allow the server to access the session + xfbml : true + + # Additional initialization code here + return + +# Load the SDK Asynchronously +((d) -> + id = "facebook-jssdk" + ref = d.getElementsByTagName("script")[0] + return if d.getElementById(id) + js = d.createElement("script") + js.id = id + js.async = true + js.src = "http://connect.facebook.net/en_US/all.js" + ref.parentNode.insertBefore js, ref + return +) document if $("#fb-root")[0]? diff --git a/app/assets/javascripts/index-itineraries-map.js.coffee b/app/assets/javascripts/index-itineraries-map.js.coffee new file mode 100644 index 000000000..8e10d11e0 --- /dev/null +++ b/app/assets/javascripts/index-itineraries-map.js.coffee @@ -0,0 +1,182 @@ +###global google:false### + +"use strict" + +window.icare = window.icare || {} +icare = window.icare + +$("#search-form-advanced-link").on 'click', (e) -> + e.preventDefault() + me = this + $("#search-form-advanced").slideToggle -> + $icon = $(me).find("i") + if $icon.hasClass "icon-chevron-up" + $icon.removeClass("icon-chevron-up").addClass("icon-chevron-down") + else + $icon.removeClass("icon-chevron-down").addClass("icon-chevron-up") + +routeColoursArray = [ + "#0000ff" + "#ff0000" + "#00ffff" + "#ff00ff" + "#ffff00" + ] + +Handlebars.registerHelper 'toLowerCase', (string) -> + if string + new Handlebars.SafeString string.toLowerCase() + else + '' + +Handlebars.registerHelper 'translate', (key) -> + if translation = $("#translations").data(key) + new Handlebars.SafeString translation + else + '' + +$ -> + if $("#index-itineraries-map")[0]? + indexItinerariesMapInit("#index-itineraries-map") + window.customMarkers = [] + window.itineraries = [] + # TODO clean this mess... directions service again? + $("#itineraries-search").on "click", -> + return false unless $("#new_itinerary_search").isValid(window.new_itinerary_search.validators) + $("#error").hide() + geocoder = new google.maps.Geocoder() + address = $("#itinerary_search_from").val() + geocoder.geocode + address: address + , (results, status) -> + if status is google.maps.GeocoderStatus.OK + $("#itinerary_search_from").val results[0].formatted_address + location = results[0].geometry.location + window.icare.search_center = location + $("#itinerary_search_start_location_lat").val results[0].geometry.location.lat() + $("#itinerary_search_start_location_lng").val results[0].geometry.location.lng() + geocoder = new google.maps.Geocoder() + address = $("#itinerary_search_to").val() + geocoder.geocode + address: address + , (results, status) -> + if status is google.maps.GeocoderStatus.OK + $("#itinerary_search_to").val results[0].formatted_address + location = results[0].geometry.location + $("#itinerary_search_end_location_lat").val results[0].geometry.location.lat() + $("#itinerary_search_end_location_lng").val results[0].geometry.location.lng() + $("#new_itinerary_search").submit() + $("#new_itinerary_search").on "keypress", (e) -> + if e and e.keyCode is 13 + e.preventDefault() + $("#itineraries-search").click() + $("#new_itinerary_search") + .bind "submit", (evt) -> + $(window.icare.itineraries).each -> + this.setMap null + window.icare.itineraries = [] + $(window.icare.customMarkers).each -> + this.setMap null + window.icare.customMarkers = [] + .bind "ajax:beforeSend", (evt, xhr, settings) -> + $("#itineraries-spinner").show() + .bind "ajax:complete", (evt, xhr, settings) -> + $("#itineraries-spinner").hide() + .bind "ajax:error", (evt, xhr, settings) -> + $("#itineraries-thumbs").html """ +

#{$("#translations").data("no_itineraries_found")}

+ """ + false + .bind "ajax:success", (evt, data, status, xhr) -> + if data.length is 0 + $("#itineraries-thumbs").html """ +

#{$("#translations").data("no_itineraries_found")}

+ """ + else + $("#itineraries-thumbs").html "" + index = 0 + window.icare.latLngBounds = new google.maps.LatLngBounds() + $(data).each -> + color = routeColoursArray[index++ % routeColoursArray.length] + drawPath this, color + this.borderColor = hexToRgba(color, 0.45) # borderColor injection, waiting for proper @data support in handlebars + $("#itineraries-thumbs").append HandlebarsTemplates["itinerary"](this) + window.icare.map.fitBounds window.icare.latLngBounds + +hexToRgba = (color, alpha = 1) -> + if color.charAt(0) is "#" then (h = color.substring(1,7)) else (h = color) + "rgba(#{parseInt(h.substring(0,2),16)}, #{parseInt(h.substring(2,4),16)}, #{parseInt(h.substring(4,6),16)}, #{alpha})" + +indexItinerariesMapInit = (id) -> + styleArray = [ + featureType: "all" + stylers: [ + ] + , + featureType: "road" + elementType: "geometry" + stylers: [ + ] + , + featureType: "poi" + elementType: "labels" + stylers: [ + visibility: "off" + ] + ] + + country_bounds = new google.maps.LatLngBounds new google.maps.LatLng(35.49292010, 6.62672010), new google.maps.LatLng(47.0920, 18.52050150) + window.icare.map = new google.maps.Map $(id)[0], + center: new google.maps.LatLng 41.87194, 12.567379999999957 + mapTypeId: google.maps.MapTypeId.ROADMAP + scrollwheel: false + styles: styleArray + zoom: 8 + window.icare.map.fitBounds(country_bounds) + + window.icare.infoWindow = new google.maps.InfoWindow + maxWidth: 400 + pixelOffset: + width: 0 + height: -35 + +drawPath = (itinerary, strokeColor = "#0000FF", strokeOpacity = 0.45) -> + window.icare.latLngBounds.extend new google.maps.LatLng(itinerary.start_location.lat, itinerary.start_location.lng) + window.icare.latLngBounds.extend new google.maps.LatLng(itinerary.end_location.lat, itinerary.end_location.lng) + overview_path = google.maps.geometry.encoding.decodePath(itinerary.overview_polyline) + return unless overview_path + new_path = [] + customMarker = new window.icare.CustomMarker overview_path[0], window.icare.map, + infoWindowContent: HandlebarsTemplates["gmaps_popup"] + title: itinerary.title + user: + image: itinerary.user.profile_picture + name: itinerary.user.name + nationality: itinerary.user.nationality + url: itinerary.url + content: itinerary.description + type: "user_profile_picture" + image: itinerary.user.profile_picture + google.maps.event.addListener customMarker, 'click', -> + window.icare.infoWindow.setContent customMarker.options.infoWindowContent + window.icare.infoWindow.open window.icare.map, customMarker + marker = new google.maps.Marker + icon: 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=A|FF0000|000000' + directionsPath = new google.maps.Polyline + clickable: false + path: overview_path + strokeColor: strokeColor + strokeOpacity: strokeOpacity + strokeWeight: 5 + icons: [ + icon: + path: google.maps.SymbolPath.CIRCLE + offset: '0%' + , + icon: + path: google.maps.SymbolPath.CIRCLE + offet: '100%' + ] + directionsPath.setMap window.icare.map + window.icare.customMarkers.push customMarker + window.icare.itineraries.push directionsPath diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee new file mode 100644 index 000000000..4738785de --- /dev/null +++ b/app/assets/javascripts/map.js.coffee @@ -0,0 +1,90 @@ +###global google:false### +###jshint shadow:true### + +"use strict" + +window.icare = window.icare || {} +icare = window.icare + +class CustomMarker + @::= new google.maps.OverlayView() + + constructor: (@position, map, opts) -> + @_div = null + @setMap map + $.extend true, @options = + infoWindowContent: null + image: "http://placehold.it/50x50" + css_classes: "markers-home" + type: "sprite" + image_overlay: null + , opts + + onAdd: -> + me = @ + div = document.createElement 'div' + div.style.position = "absolute" + + switch @options.type + when "user_profile_picture" + div.setAttribute "class", "#{@options.css_classes} arrow_box" + div.style.width = '31px' + div.style.height = '31px' + img = document.createElement 'img' + img.setAttribute "width", "25px" + img.setAttribute "height", "25px" + img.setAttribute "alt", "" + img.src = @options.image + div.appendChild img + when "sprite" + div.setAttribute "class", @options.css_classes + div.style.border = "0px solid none" + div.style.width = '32px' + div.style.height = '37px' + div.style.cursor = 'pointer' + + @_div = div + + @getPanes().overlayMouseTarget.appendChild div + + google.maps.event.addDomListener div, 'click', -> + google.maps.event.trigger me, 'click' + + draw: -> + point = @getProjection().fromLatLngToDivPixel @position + width = parseInt @_div.style.width, 10 + height = parseInt @_div.style.height, 10 + height += 5 if @type is "user_profile_picture" + if point + @_div.style.left = "#{point.x - width/2}px" + @_div.style.top = "#{point.y - height}px" + return + + onRemove: -> + if @_div + @_div.parentNode.removeChild @_div + @_div = null + return + +window.icare.CustomMarker = CustomMarker + +random_nearby_position = (latLng, maxDist = 0.5, km = true) -> + #very very very very special thanks to http://www.geomidpoint.com/random/calculation.html + DEG_TO_RAD = Math.PI / 180 + RAD_TO_DEG = 180 / Math.PI + startlat = latLng[0] * DEG_TO_RAD + startlng = latLng[1] * DEG_TO_RAD + rand1 = Math.random() + rand2 = Math.random() + radiusEarthM = 3960.056052 + radiusEarthKm = 6372.796924 + maxDistRad = maxDist / (if km then radiusEarthKm else radiusEarthM) + dist = Math.acos(rand1 * (Math.cos(maxDistRad) - 1) + 1) + brg = 2 * Math.PI * rand2 + lat = Math.asin( Math.sin(startlat) * Math.cos(dist) + Math.cos(startlat) * Math.sin(dist) * Math.cos(brg) ) + lng = startlng + Math.atan2( Math.sin(brg) * Math.sin(dist) * Math.cos(startlat), Math.cos(dist) - Math.sin(startlat) * Math.sin(lat) ) + if (lng < -1 * Math.PI) + lng = lng + 2 * Math.PI + else if (lng > Math.PI) + lng = lng - 2 * Math.PI + [lat * RAD_TO_DEG, lng * RAD_TO_DEG] diff --git a/app/assets/javascripts/navbar.js.coffee b/app/assets/javascripts/navbar.js.coffee new file mode 100644 index 000000000..f4cccc62c --- /dev/null +++ b/app/assets/javascripts/navbar.js.coffee @@ -0,0 +1,12 @@ +"use strict" + +$("a[id^=navbar-notifications-]").popover().click (e) -> + e.preventDefault() + $me = $(this) + $("a[id^=navbar-notifications-]").each -> + $this = $(this) + if $me.attr("id") is $this.attr("id") + $(this).popover('toggle') + else + $(this).popover('hide') + return diff --git a/app/assets/javascripts/new-itinerary-map.js.coffee b/app/assets/javascripts/new-itinerary-map.js.coffee new file mode 100644 index 000000000..01e313cd2 --- /dev/null +++ b/app/assets/javascripts/new-itinerary-map.js.coffee @@ -0,0 +1,309 @@ +### +Codename Icare + +Microdonations + +100% facebook, interests, friends, ecc. + +after mutual friends + +Itineraries + people in the car: bool + visibility: friends, friends of friends, public + periodicity: think about it +### + +# From https://google-developers.appspot.com/maps/customize_ae458c7692ac994187feb6f58834b6af.frame + +###global google:false### + +"use strict" + +window.icare = window.icare || {} +icare = window.icare + +getJSONRoute = (route) -> + # TODO server side, basing on start location, end location and via waypoints + + data = + start_location: null + end_location: null + via_waypoints: [] + overview_path: [] + overview_polyline: null + + rleg = route.legs[0]; + data.start_location = + 'lat': rleg.start_location.lat() + 'lng': rleg.start_location.lng() + data.end_location = + 'lat': rleg.end_location.lat(), + 'lng': rleg.end_location.lng() + + for waypoint in rleg.via_waypoints + data.via_waypoints.push [ waypoint.lat(), waypoint.lng() ] + + for point in route.overview_path + data.overview_path.push [point.lat(), point.lng() ] + + data.overview_polyline = route.overview_polyline.points + + data + +wizardPrevStep = -> + step = (Number) $("#new_itinerary").data "step" + return if step <= 1 + + last_step = (Number) $("#new_itinerary").data "lastStep" + + $("#wizard-step-#{step}-content").fadeOut -> + $("#wizard-next-step-button").removeAttr "disabled" + $("#wizard-next-step-button").show() + $("#new_itinerary_submit").attr "disabled", "disabled" + $("#new_itinerary_submit").hide() + $("#wizard-step-#{step}-title").addClass("hidden-phone").removeClass "active" + $("#new_itinerary").data "step", --step + $("#wizard-step-#{step}-title").find(".icon-check").addClass("icon-check-empty").toggleClass "icon-check" + $("#wizard-step-#{step}-title").removeClass("done").removeClass("hidden-phone").addClass "active" + $("#wizard-step-#{step}-content").fadeIn() + if step is 1 + $("#wizard-prev-step-button").attr "disabled", "disabled" + $("#wizard-prev-step-button").hide() + +wizardNextStep = -> + # Run validations + if $("#itinerary_route_json_object").val() is "" + $("#error").text($("#new_itinerary_route").data "getRouteBeforeText").show() + return false + + valid = true + $('#new_itinerary [data-validate]:input:visible').each -> + settings = window.new_itinerary + unless $(this).isValid(settings.validators) + valid = false + return + return false unless valid + + step = (Number) $("#new_itinerary").data "step" + last_step = (Number) $("#new_itinerary").data "lastStep" + + if step is last_step + return false + + $("#wizard-step-#{step}-content").fadeOut -> + $("#wizard-step-#{step}-title").removeClass("active").addClass("done").addClass "hidden-phone" + $("#wizard-step-#{step}-title").find(".icon-check-empty").addClass("icon-check").toggleClass "icon-check-empty" + $("#new_itinerary").data "step", ++step + if step is last_step + lastStepInit() + $("#wizard-step-#{step}-title").removeClass("hidden-phone").addClass "active" + $("#wizard-step-#{step}-content").fadeIn() + if step > 1 + $("#wizard-prev-step-button").removeAttr "disabled" + $("#wizard-prev-step-button").show() + if step is last_step + $("#wizard-next-step-button").attr "disabled", "disabled" + $("#wizard-next-step-button").hide() + $("#new_itinerary_submit").removeAttr "disabled" + $("#new_itinerary_submit").show() + +dateFieldToString = (field_id) -> + values = $("select[id^=#{field_id}] option:selected") + "#{$(values[0]).text()} #{$(values[1]).text()} #{$(values[2]).text()} \u2014 #{$(values[3]).text()}:#{$(values[4]).text()}" + +lastStepInit = -> + $("#itinerary-preview-title").text $("#itinerary_title").val() + $("#itinerary-preview-description").text $("#itinerary_description").val() + $("#itinerary-preview-vehicle").text $("#itinerary_vehicle option:selected").text() + $("#itinerary-preview-smoking_allowed").text if $("#itinerary_smoking_allowed").attr("checked")? then $("#itinerary-preview").data("true_text") else $("#itinerary-preview").data("false_text") + $("#itinerary-preview-pets_allowed").text if $("#itinerary_pets_allowed").attr("checked")? then $("#itinerary-preview").data("true_text") else $("#itinerary-preview").data("false_text") + $("#itinerary-preview-fuel_cost").text $("#itinerary_fuel_cost").val() + $("#itinerary-preview-tolls").text $("#itinerary_tolls").val() + + round_trip = $("#itinerary_round_trip").attr("checked")? + $("#itinerary-preview-round_trip").text if round_trip then $("#itinerary-preview").data("true_text") else $("#itinerary-preview").data("false_text") + $("#itinerary-preview-leave_date").text dateFieldToString("itinerary_leave_date") + + if round_trip + $("#itinerary-preview-return_date").text dateFieldToString("itinerary_return_date") + $(".itinerary-preview-return").show() + else + $(".itinerary-preview-return").hide() + + route = window.itinerary_route_json_object.route + url_builder = $("#itinerary-preview-image") + .data("staticMapUrlBuilder") + .replace("%{end_location}", "#{route.end_location.lat},#{route.end_location.lng}") + .replace("%{start_location}", "#{route.start_location.lat},#{route.start_location.lng}") + .replace("%{overview_polyline}", "#{route.overview_polyline}") + $("#itinerary-preview-image").attr "src", url_builder + +$ -> + if $("#new_itinerary")[0]? + createRouteMapInit("#new-itinerary-map") + $("#wizard-next-step-button").on "click", wizardNextStep + $("#wizard-prev-step-button").on "click", wizardPrevStep + $('input[name="itinerary[recurrent]"]').change -> + if (Boolean) $(this).val() is "true" + $("#single").fadeOut -> + $("#daily").fadeIn() + else + $("#daily").fadeOut -> + $("#single").fadeIn() + $('#itinerary_round_trip').change -> + status = $(this).attr("checked") + $('select[id^=itinerary_return_date]').each -> + $(this).attr "disabled", (if status then null else "disabled") + +createRouteMapInit = (id) -> + recalcHeight = -> + $("#map").height $(window).height() - $("form").height() - $("#elevation").height() + map and google.maps.event.trigger map, "resize" + $(window).resize recalcHeight + recalcHeight() + + # for o of google.maps.DirectionsTravelMode + # $("#mode").append new Option(o) + + styleArray = [ + featureType: "all" + stylers: [ + ] + , + featureType: "road" + elementType: "geometry" + stylers: [ + ] + , + featureType: "poi" + elementType: "labels" + stylers: [ + visibility: "off" + ] + ] + + country_bounds = new google.maps.LatLngBounds new google.maps.LatLng(35.49292010, 6.62672010), new google.maps.LatLng(47.0920, 18.52050150) + map = new google.maps.Map $(id)[0], + center: new google.maps.LatLng 41.87194, 12.567379999999957 + mapTypeId: google.maps.MapTypeId.ROADMAP + scrollwheel: false + styles: styleArray + zoom: 8 + map.fitBounds(country_bounds) + + dr = new google.maps.DirectionsRenderer + map: map + draggable: true + preserveViewport: true + + hoverMarker = new google.maps.Marker(map: map) + + $("#elevation").mouseleave -> + hoverMarker.setVisible false + $("#elevation-hover").hide() + + ds = new google.maps.DirectionsService() + + google.maps.event.addListener dr, "directions_changed", -> + route = dr.getDirections().routes[0] + route_json_object = getJSONRoute route + $("#from-helper").text route.legs[0].start_address + $("#to-helper").text route.legs[0].end_address + $("#itinerary_route_json_object").val JSON.stringify(route_json_object) + window.itinerary_route_json_object.route = route_json_object + $("#new_itinerary_submit").removeAttr "disabled" + $("#distance").text route.legs[0].distance.text + $("#duration").text route.legs[0].duration.text + $("#route-helper").show() + $("#result").show() + $("#itinerary_title").val "#{$("#itinerary_route_from").val()} - #{$("#itinerary_route_to").val()}".substr(0, 40).capitalize() + route_km = route.legs[0].distance.value / 1000 + route_gasoline = route_km * (Number) $("#fuel-help").data("avg_consumption") + $("#fuel-help-text").text $("#fuel-help").data("text").replace("{km}", route_km.toFixed(2)).replace("{est}", parseInt(route_gasoline, 10)) + $("#fuel-help").show() + path = route.overview_path + map.fitBounds(dr.directions.routes[0].bounds) + + $("#new_itinerary_route").submit -> + return false unless $("#new_itinerary_route").isValid window.new_itinerary_route.validators # Don't know why! + $("#itineraries-spinner").show() + $("#error").hide() + $("#result").hide() + $("#route-helper").hide() + $("#distance").text("") + $("#duration").text("") + ds.route + origin: $("#itinerary_route_from").val() + destination: $("#itinerary_route_to").val() + travelMode: "DRIVING" #$("#mode").val() + avoidHighways: $("#itinerary_route_avoid_highways").attr("checked")? + avoidTolls: $("#itinerary_route_avoid_tolls").attr("checked")? + , (result, status) -> + $("#itineraries-spinner").hide() + if status is google.maps.DirectionsStatus.OK + dr.setDirections result + dr.setOptions + polylineOptions: + strokeColor:"#0000ff" + strokeWeight:5 + strokeOpacity:0.45 + dr.map.fitBounds(dr.directions.routes[0].bounds) + # dr.setOptions + # suppressMarkers: true + else + switch status + when "NOT_FOUND" + message = $("#new_itinerary_route").data "notFoundText" + when "ZERO_RESULTS" + message = $("#new_itinerary_route").data "zeroResultsText" + else + message = status + $("#error").text(message).show() + recalcHeight() + false + + drawElevation = (r) -> + max = writeStats(r) + drawGraph r, max + + writeStats = (r) -> + prevElevation = r[0].elevation + climb = 0 + drop = 0 + max = 0 + i = 1 + + while i < r.length + diff = r[i].elevation - prevElevation + prevElevation = r[i].elevation + if diff > 0 + climb += diff + else + drop -= diff + max = r[i].elevation if r[i].elevation > max + i++ + max = Math.ceil(max) + $("#climb-drop").text "Climb: " + Math.round(climb) + "m Drop: " + Math.round(drop) + "m" + max + + drawGraph = (r, max) -> + ec = $("#elevation-chart").empty() + width = Math.max(1, Math.floor(Math.min(11, ec.width() / r.length)) - 1) + height = 100 + $.each r, (i, e) -> + barHeight = Math.round(e.elevation / max * height) + bar = $("
") + ec.append bar + bar.mouseenter(-> + offset = bar.find("div").offset() + offset.top -= 25 + offset.left -= 3 + hoverMarker.setVisible true + hoverMarker.setPosition e.location + $("#elevation-hover").show().text(Math.round(e.elevation) + "m").offset offset + map.panTo e.location unless map.getBounds().contains(e.location) + ).click -> + map.panTo e.location + + $(".share").click -> + $(this).find("input").focus().select() diff --git a/app/assets/javascripts/pages.js.coffee b/app/assets/javascripts/pages.js.coffee new file mode 100644 index 000000000..0fefc5d99 --- /dev/null +++ b/app/assets/javascripts/pages.js.coffee @@ -0,0 +1,40 @@ +###global clientSideValidations:false### +"use strict" + +# Prevent disabled links from being clicked +$("a.disabled").on 'click', (e) -> + e.preventDefault() + +$ -> + # Client Side Validations + clientSideValidations.callbacks.element.fail = (element, message, callback) -> + if (!element.data('valid')) + element.closest('div.control-group').addClass "error" + + if element.closest('form').hasClass "form-inline" + error_message = "" + $error_container = element.parent().find "label.message" + else + error_message = "#{message}" + if element.parent().hasClass("input-prepend") or element.parent().hasClass("input-append") + $error_container = element.parent().parent().find "span.help-inline" + else + $error_container = element.parent().find "span.help-inline" + + if $error_container[0] + $error_container.text(message).show() + else + if element.parent().hasClass("input-prepend") or element.parent().hasClass("input-append") + element.parent().after error_message + else + element.parent().find("#{element[0].tagName}:last").after error_message + return + + clientSideValidations.callbacks.element.pass = (element, callback) -> + element.closest('div.control-group').removeClass "error" + element.parent().find('label.message').hide() + if element.parent().hasClass("input-prepend") or element.parent().hasClass("input-append") + element.parent().parent().find('span.help-inline').hide() + else + element.parent().find('span.help-inline').hide() + return diff --git a/app/assets/javascripts/references.js.coffee b/app/assets/javascripts/references.js.coffee new file mode 100644 index 000000000..5f7aa87da --- /dev/null +++ b/app/assets/javascripts/references.js.coffee @@ -0,0 +1,5 @@ +"use strict" + +$ -> + $(".reference-rating").on "click", -> + $("#incoming_reference_outgoing_reference_attributes_rating").val $(this).data("rating") diff --git a/app/assets/javascripts/templates/gmaps_popup.hbs.haml b/app/assets/javascripts/templates/gmaps_popup.hbs.haml new file mode 100644 index 000000000..1cff2c57f --- /dev/null +++ b/app/assets/javascripts/templates/gmaps_popup.hbs.haml @@ -0,0 +1,14 @@ +.gmaps-popup + .gmaps-popup-image + %img{ width: 50, height: 50, alt: "", src: "{{user.image}}" } + .gmaps-popup-text + %p.gmaps-popup-p-title + %a{ href: "{{url}}" } + {{title}} + %p.gmaps-popup-p-content + {{#if user.nationality}} + %i{ class: "flag-{{toLowerCase user.nationality}}" } + {{/if}} + {{user.name}} + .gmaps-popup-div-details + {{content}} diff --git a/app/assets/javascripts/templates/itinerary.hbs.haml b/app/assets/javascripts/templates/itinerary.hbs.haml new file mode 100644 index 000000000..133b595d8 --- /dev/null +++ b/app/assets/javascripts/templates/itinerary.hbs.haml @@ -0,0 +1,28 @@ +.span6.itinerary-thumbnail.border-box + .caption.itinerary-thumb-stroke{ style: "border-color: {{borderColor}}" } + %table + %tbody + %tr + %td.user-profile-picture + %img{ src: "{{user.profile_picture}}", width: 50, height: 50, alt: "" } + %td + %strong.it-title + %a.pre-line{ href: "{{url}}" }>< + {{title}} + %p.it-user + {{#if user.nationality}} + %i{ class: "flag-{{toLowerCase user.nationality}}" } + {{/if}} + {{user.name}} + %p + %i.icon-arrow-up + {{leave_date}} + {{#if round_trip}} + %i.icon-arrow-down + {{return_date}} + {{/if}} + .btn-toolbar + %a.btn.btn-mini{ href: "#" } + {{translate "show_on_map"}} + %a.btn.btn-mini.btn-primary{ href: "{{url}}" } + {{translate "read_more"}} diff --git a/app/assets/javascripts/users.js.coffee b/app/assets/javascripts/users.js.coffee new file mode 100644 index 000000000..65b2b80f4 --- /dev/null +++ b/app/assets/javascripts/users.js.coffee @@ -0,0 +1,41 @@ +"use strict" + +$ -> + setTimeout -> + $(".unread").removeClass "unread" + , 5000 + + if $("#fuel-cost-calculator")[0]? + $("#save-fuel-cost").on "click", -> + $("#user_vehicle_avg_consumption").val $("#fuel_price").val() + + $("select[id='user_nationality']").change (event) -> + $this = $(this) + code = $this.val() + flag = if code != "" then "flag-#{code.toLowerCase()}" else "" + $("#flag").attr "class", flag + +if $("#profilenav")[0]? + $win = $(window) + $nav = $('#profilenav') + navTop = $nav.length && $nav.offset().top - 40 + isFixed = 0 + + processScroll = -> + scrollTop = $win.scrollTop() + if (scrollTop >= navTop && !isFixed) + isFixed = 1 + $nav.addClass "profilenav-fixed" + else if (scrollTop <= navTop && isFixed) + isFixed = 0 + $nav.removeClass "profilenav-fixed" + return + + processScroll() + + $nav.on "click", -> + setTimeout -> $win.scrollTop($win.scrollTop() - 47), + 10 unless isFixed + return + + $win.on "scroll", $.throttle(250, processScroll) diff --git a/app/assets/stylesheets/application.css.less b/app/assets/stylesheets/application.css.less new file mode 100644 index 000000000..d31fbbf68 --- /dev/null +++ b/app/assets/stylesheets/application.css.less @@ -0,0 +1,217 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + *= require_tree . +*/ + +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +.pacifico { + font-family: "Pacifico"; + font-weight: 400; +} + +h1.pacifico { + line-height: 44px; +} + +.width-max { + max-width: 100%; +} + +input.width-max { + .box-sizing(border-box); + height: 28px !important; +} + +.full-width { + width: 100%; +} + +.border-box { + .box-sizing(border-box); +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +.align-justify { + text-align: justify; +} + +.vertical-align-top { + vertical-align: top; +} + +.vertical-align-middle td { + vertical-align: middle !important; +} + +.no-margin { + margin: 0; +} + +.diowa-logo { + position: relative; + display: inline-block; + top: -1px; + margin-right: 3px; + .size(25px, 78px); + background: url("data:image/svg+xml;base64,PHN2ZyBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGhlaWdodD0iMTQ3IiBpbWFnZS1yZW5kZXJpbmc9Im9wdGltaXplUXVhbGl0eSIgc2hhcGUtcmVuZGVyaW5nPSJnZW9tZXRyaWNQcmVjaXNpb24iIHRleHQtcmVuZGVyaW5nPSJnZW9tZXRyaWNQcmVjaXNpb24iIHZpZXdCb3g9IjAgMCA1ODIuODA0IDE4Ny41MzIiIHdpZHRoPSI0NTgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQogIDxkZWZzPjwvZGVmcz4NCiAgPHBhdGggZD0ibTMuMDA0MiwxMzguNDQzIDc4LjYxMDUsNDUuNjIzIDc4Ljc1NDMtNDUuNjI5di04OS44OTI0bC03OC43NjAzLTQ1LjA4OTgtNzguNjA0NSw0NS4zMjExeiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMzMzMzMzIiBzdHJva2Utd2lkdGg9IjYuMDA4NCI+PC9wYXRoPg0KICA8cGF0aCBkPSJtMy4wMDQyLDQ4Ljc3NTkgNzguNjEwNSw0NC43NjI0IDc4Ljc1NDctNDQuOTg3N20tNzguNzU0NywxMzUuNTE2di05MC41MjgxIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMzMzMiIHN0cm9rZS13aWR0aD0iNi4wMDg0Ij48L3BhdGg+DQogIDxwYXRoIGQ9Im02OC43NzMzLDEyMi44MTZjLTEuNTAwNiwxLjA0MS00LjYzODUsMi44NDItOS4zMzEsNC4yNzVzLTEwLjkzNjcsMi41MDI1LTE4LjY0MSwyLjEyNGMuOTUwOCwxLjUyOTEgMi4yODAyLDQuMzY5NiAyLjkwMDUsNS45MTIyIDQuMDg4Ny4yNTY5IDEwLjM3OC0uNDMyNiAxMy43OTgyLTEuMTUwNnYxMC41ODA3IDEwLjU2NTdjMS42ODIzLjk2MTMgMy4zNjQ3LDEuOTQ4MiA1LjA0NywyLjkyMzF2LTExLjY3NTgtMTEuNjc1OGMuNTczOC4yMTYzLjk1MDguMTA4MiAxLjA5MzUtLjE4OTMtLjMzMDUtLjM1MTUtLjc2MzEtLjc1ODYtMS4yODEzLTEuMjcyMyAzLjMyNTYtMS4wMDA0IDcuMTc3LTIuMTY0NSAxMC4xMjU2LTMuODE1MyAuNjY4NC41MDAyIDEuMTQ3Ni42NjI0IDEuNDMxNS40NDYxLTEuNzEzOS0yLjM1MzgtMy40MjkzLTQuNzA3Ni01LjE0MzItNy4wNDc4eiIgZmlsbD0iIzMzMzMzMyIgZmlsbC1ydWxlPSJub256ZXJvIj48L3BhdGg+DQogIDxwYXRoIGQ9Im0zMC4wODU0LDExMC4wODRjLS40OTU3LDQuNDExNi0xLjUyMzEsOC4yMTY1LTMuNzUwNywxMC45MTg3LTIuMjIxNiwyLjY5MzItNS42NzY0LDQuMzAyLTExLjAxMzMsNC4zNDI2IDEuNTA4MSwyLjA5NjkgNC40MjUyLDYuNTIwNiA1LjQ3OTYsOC40ODIzIDUuNTExMi0uMDEzNSA5LjM0OS0yLjI0NTYgMTEuOTczMi01LjYyNjggMi42MzMyLTMuMzk0NyA0LjA3MjItNy45Njg2IDQuODAzNy0xMi41OTY1IC43MzkuNDE5MSAxLjE3OTEuMjgzOSAxLjI5NzgtLjA4MTFsLTEuMTE3Ni0xLjAyNzRjNS42NDQ5LDMuMjQ2IDExLjI4MjIsNi40OCAxNi45MzQ2LDkuNzQxMXYtNy41MjI1Yy0zLjY4NjEtMi4xMTA0LTcuMzcyMy00LjIyNjktMTEuMDUzOS02LjM0NDhsLTExLjAzNzQtNi4zMzI4LTExLjA1MzktNi4zNTg0LTExLjAzODktNi4zMzEzdjcuNDgxOWM2LjUyNjYsMy43MzQyIDEzLjA1OTIsNy41MDkgMTkuNTc2OCwxMS4yNTUyeiIgZmlsbD0iIzMzMzMzMyIgZmlsbC1ydWxlPSJub256ZXJvIj48L3BhdGg+DQogIDxwYXRoIGQ9Im01Mi41MjM3LDEwMi4xOTZjLTEuMzQ0NC0zLjQwODItMy45Ny04Ljk1NTUtNi4xNjQ2LTEyLjYzNTYtMS4wNTMuMjk3NC0yLjEyMjUuNTY3OC0zLjE4MjkuODY2NyAuNjIwNCwxLjAwMDQgMS4zNDQ0LDIuMjk5NyAyLjAxMTMsMy42NTE2bC0xNC43NjQxLTguNDQxOC0xNC43NjQxLTguNDQxOHY3LjQ2ODRjNS4xMTc2LDIuOTM1MSAxMC4yMzgzLDUuODY4NyAxNS4zNTQ0LDguNzkzM2wxNS4zNDU0LDguNzkzM3YtNS44Mjk2YzEuMTI1MSwyLjM4MDggMi4xOTQ2LDQuODQyNyAyLjc5OTksNi42NjkzbDMuMzY0Ny0uODkzN3oiIGZpbGw9IiMzMzMzMzMiIGZpbGwtcnVsZT0ibm9uemVybyI+PC9wYXRoPg0KICA8cGF0aCBkPSJtNjAuMjk4NSwxMDUuMDM4Yy0xLjM0NDQtMy4zNDIyLTQuMDMzMS04Ljc5NDgtNi4yMTg3LTEyLjU0MS0uOTk4OS4zMTA5LTIuMDEyOC42MDgzLTMuMDE5Mi45MDU4IDIuMDY4NCwzLjU1ODUgNC43NTcxLDkuMzIyIDUuOTI4OCwxMi40MzI4IDEuMDk5NS0uMjU2OSAyLjIwMDYtLjUyNzIgMy4zMDkxLS43OTc2eiIgZmlsbD0iIzMzMzMzMyIgZmlsbC1ydWxlPSJub256ZXJvIj48L3BhdGg+DQogIDxwYXRoIGQ9Im0xNDUuMTc2LDg2LjYwNzZjLTUuMTU4MiwyLjk2ODEtMTAuMzA4OSw1LjkzNjMtMTUuNDU5NSw4Ljg3NzQtLjAzOTEtMy42ODYxLS4wNzgxLTcuMzk5My0uMTE4Ny0xMS4wODU0IDEuMTI2Ni0uNzczNiAxLjUxMjYtMS41MTg2IDEuNTY4Mi0yLjI1MDFsLTguNjIzNSw0LjU2NjRjLjAzMTUsNC4yNjkuMDcwNiw4LjU1MTQuMTA5NywxMi44MjA0LTMuODc1NCwyLjIyMzEtNy43NTA4LDQuNDU5Ny0xMS42MjQ3LDYuNjgxM2wtMTEuNjIzMiw2LjY2Nzh2Ny42NTc3YzYuNDQxLTMuNjg2MSAxMi44ODM0LTcuMzk5MyAxOS4zMjU5LTExLjExMjUtMi44MDQ0LDUuODQwMS02LjI2MDcsMTEuOTI2Ni0xMC4wNDksMTcuNzkzOHMtNy45MDcsMTEuNTA2LTEyLjAyNTgsMTYuMzg0OGMxLjg1MDYuNTI3MiAzLjgxMDgsMS45MTA3IDQuOTg1NCwzLjIyNSAzLjkwNTQtNC45MDU4IDcuODA5NC0xMC40NTE2IDExLjQyNjQtMTYuMTgyIDMuNjExLTUuNzIgNi45NTQ3LTExLjY4MTggOS43MjYxLTE3LjQ1NDMgLjA1NTYsNi44ODQxLjExMTIsMTMuNzU0Ny4xNjUyLDIwLjYyNjcgLjExMTIsMS44NzAxLS43MTgsMi4zNTUzLTYuMDQ3NCw1LjQzMzEtMS40NDk1LjgzODItMy4wNzkzLDEuNzYxOS00LjUzNjMsMi41NDkgLjcyNCwxLjYyNTMgMS41MTI2LDQuNDQ0NyAxLjc4NzUsNi4zNDE4IDEwLjkzOTctNi4zMTQ4IDE1Ljk0MDItOS4yMDE4IDE1LjgyOTEtMTguMjQxNC0uMDM5MS00LjQ3MTctLjA4NTYtOC45MzE0LS4xMTcyLTEzLjQwMzJzLS4wNzA2LTguOTQzNS0uMTAyMS0xMy40MTY3YzUuMTM0Mi0yLjk0MTEgMTAuMjY5OC01Ljg5NDIgMTUuNDA0LTguODQ4OHYtNy42MzA2eiIgZmlsbD0iIzMzMzMzMyIgZmlsbC1ydWxlPSJub256ZXJvIj48L3BhdGg+DQogIDxwYXRoIGQ9Im00NC41NTA2LDUxLjgwNDJjLjQxNzYuMDU0MS44MzUyLjEwODIgMS4yNTI4LjE3MTJsMTcuNjc5NiwxMC40MDY1IDE3LjY0ODEsMTAuMzg4NSAxMC4zMjM5LTYuMDY0YzMuNDM4My0yLjAyNjMgNi44OTMxLTQuMDM2MSAxMC4zMjM5LTYuMDU0OWwtNi43NTM0LTMuOTM3Yy00LjcxMzYsMi43NTc4LTkuNDI3MSw1LjUyMzItMTQuMTQwNyw4LjI4ODYtNC4zNzQxLTIuNTY3MS04Ljc0NjctNS4xMzg3LTEzLjEyMDgtNy43MDI3cy04Ljc0ODItNS4xMzU3LTEzLjEyMDgtNy43MDI3YzguMTEyOC0zLjc3NDggMTYuMDU3NC02LjI2MzcgMjUuMDIwNC03LjA1NTMgOC45OTMtLjc5MzEgMTkuMDU1NS4wOTkxIDMxLjM3MjcsMy4wNzMzLS4yMTYzLTEuNzkzNS0uMDkzMS00LjIyNjkuMjc3OS01Ljk4MjgtMTMuMzk4Ny0zLjI0My0yNC42ODA5LTQuMDMzMS0zNS40NTI0LTIuNzEyOC0xMC43NTUsMS4zMTczLTIxLjA0ODgsNC43NjYxLTMyLjQ2OTIsMTAuMDQ2LS42MDIzLS4xNjIyLTEuMzc1OS0uMTcxMi0xLjkxNjctLjA0NTFsMy4wNzQ4LDQuODgzM3oiIGZpbGw9IiMzMzMzMzMiIGZpbGwtcnVsZT0ibm9uemVybyI+PC9wYXRoPg0KICA8cGF0aCBkPSJtMjM1LjI3MSw3Mi4yMjIxYy01LjYyMDgtNS4yNDIzLTExLjU4ODYtNy44NjItMTcuOTM1LTcuODYyLTUuMDUzLDAtOS4wOTM3LDIuMzM1OC0xMi4xMjQ5LDcuMDA4OC0zLjAzMjcsNC43MDYxLTQuNTQ4MywxMS43Nzk0LTQuNTQ4MywyMS4yODMxIDAsOC45OTkgMS40MjEsMTUuNzg4NSA0LjI2MjksMjAuMzAzOCAyLjg0Miw0LjU0NjggNi43ODk1LDYuODIxIDExLjg0MSw2LjgyMSA2LjQ3NCwwIDEyLjY2MjYtMy44ODQ0IDE4LjUwNDMtMTEuNTg4NnYtMzUuOTY2MXptMCw1OS40NTg4Yy0yLjk2ODEsMi4xNzgtNi4wNjI1LDMuODUxNC05LjI4MjksNS4wMi0zLjI1MiwxLjE2ODYtNi42OTQ4LDEuNzM2NC0xMC4zMjU0LDEuNzM2NC02LjAzMDksMC0xMS4zOTk0LTEuMDcyNS0xNi4wNDA4LTMuMjgzNi00LjY3My0yLjE3OC04LjU1NzQtNS4yNzIzLTExLjcxNDgtOS4yODI5cy01LjUyNjItOC44NDEzLTcuMTY4LTE0LjQ5MzdjLTEuNjQxOC01LjYyMDgtMi40NjM0LTExLjg3MjUtMi40NjM0LTE4LjcyNTEgMC03LjI2MjYuOTc5NC0xMy43OTgyIDIuOTM2Ni0xOS41NDUyIDEuOTI1Ny01Ljc3ODYgNC42MDk5LTEwLjY3MzkgOC4wNTI3LTE0LjcxNDUgMy40MDk3LTQuMDQyMSA3LjQ1MTktNy4xMzY0IDEyLjA5MzMtOS4zMTYgNC42NzMtMi4xNDY1IDkuNjYzLTMuMjIwNSAxNS4wMjk5LTMuMjIwNSAzLjUzNzQsMCA2Ljg1MjUuMzc4NSAxMC4wMDk5LDEuMjAwMiAzLjEyNTguNzg4NiA2LjA5NCwxLjk1NzIgOC44NzI5LDMuNTA0NHYtMzYuMzQ0NmgyMS42NjE3djEyMi4xMDdoLTIxLjY2MTd2LTQuNjQxNXptNTAuMjI3LDQuNjQxNWgtMjEuNjI4NnYtODguMzUxNmgyMS42Mjg2djg4LjM1MTZ6bTAtOTkuMzcwOWgtMjEuNjI4NnYtMjIuNzM1N2gyMS42Mjg2djIyLjczNTd6bTQ5LjQ3MTQsOC45MDQ0YzYuNDA5NCwwIDEyLjE4OCwxLjA0MjUgMTcuMzM1NiwzLjEyNTggNS4xNDYyLDIuMTE2NCA5LjUzNTMsNS4xMTYxIDEzLjE2NzMsOS4wNjIxIDMuNjYyMSwzLjk0NzUgNi40NDEsOC44MDk4IDguNDMxMiwxNC41MjUyIDEuOTU3Miw1Ljc0NyAyLjk2ODEsMTIuMjUyNiAyLjk2ODEsMTkuNTQ2NyAwLDcuMjYyNi0xLjAxMDksMTMuNzY2Ny0zLjAzMTIsMTkuNTEzNy0xLjk5MDMsNS43MTU1LTQuODAwNywxMC41Nzc3LTguNDYyOCwxNC41NTY4LTMuNjMyMSwzLjk3OS04LjAyMTIsNy4wMTAzLTEzLjIzMDQsOS4xMjUyLTUuMTc5MiwyLjA4NDktMTAuODk0NywzLjEyNTgtMTcuMTc3OSwzLjEyNTgtNi4yNTE3LDAtMTEuOTM1Ni0xLjAwOTQtMTcuMDgzMy0zLjA5NDMtNS4xNDYyLTIuMDUxOS05LjU2NjgtNS4wNTE1LTEzLjIzMDQtOC45Njc1cy02LjUwNDEtOC43NDY3LTguNTU3NC0xNC41MjUyYy0yLjAyMDMtNS43Nzg2LTMuMDYyOC0xMi4zNDU3LTMuMDYyOC0xOS43MzQ1IDAtNy4yOTQyIDEuMDEwOS0xMy43OTk3IDMuMDMxMi0xOS41NDY3IDEuOTkwMy01LjcxNTUgNC43OTkyLTEwLjU3NzcgOC40NjI4LTE0LjUyNTIgMy42MzA2LTMuOTQ2IDguMDUxMi02Ljk0NTcgMTMuMjMwNC05LjA2MjEgNS4xNzkyLTIuMDgzNCAxMC44OTQ3LTMuMTI1OCAxNy4yMDk1LTMuMTI1OHptMCw3NC4wNzg3YzIuODEwNCwwIDUuNDMxNi0uNTY5MyA3Ljg2Mi0xLjczNjQgMi40MDAzLTEuMTY4NiA0LjQ1MjItMi45MzY2IDYuMTI1NS01LjI3MzkgMS43MDY0LTIuMzM3MyAzLjAzMjctNS4yNDIzIDMuOTc5LTguNzE1MSAuOTc5NC0zLjQ0MTMgMS40NTI1LTcuNDgzNCAxLjQ1MjUtMTIuMDkzMyAwLTQuNjQzLS40NzMyLTguNjgzNi0xLjQ1MjUtMTIuMTI2NC0uOTQ2My0zLjQ3MjgtMi4yNzI3LTYuMzQ2My0zLjk3OS04LjY1Mi0xLjY3MzMtMi4zMDQyLTMuNzI1Mi00LjA3MjItNi4xMjU1LTUuMjQwOC0yLjQzMDQtMS4xNjg2LTUuMDUxNS0xLjczNjQtNy44NjItMS43MzY0LTIuOTA1MSwwLTUuNTU3Ny41Njc4LTcuOTU4MSwxLjczNjQtMi40MzA0LDEuMTY4Ni00LjQ4MzcsMi45MzY2LTYuMTU3MSw1LjI0MDgtMS43MDQ5LDIuMzA1Ny0zLjAzMTIsNS4xNzkyLTMuOTc5LDguNjUyLS45NDYzLDMuNDQyOC0xLjQ1MjUsNy40ODM0LTEuNDUyNSwxMi4xMjY0IDAsNC42MDk5LjUwNjIsOC42NTIgMS40NTI1LDEyLjA5MzMgLjk0NzgsMy40NzI4IDIuMjc0Miw2LjM3NzkgMy45NzksOC43MTUxIDEuNjczMywyLjMzNzMgMy43MjY3LDQuMTA1MiA2LjE1NzEsNS4yNzM5IDIuNDAwMywxLjE2NzEgNS4wNTMsMS43MzY0IDcuOTU4MSwxLjczNjR6bTE2MS4wMy03MS45NjM3LTI2LjQ2MDksODguMzUxNmgtMTUuMDYzbC0xOS4wNzIxLTY1LjA0ODEtMTkuMDQwNSw2NS4wNDgxaC0xNS4xODc3bC0yNi42NTE2LTg4LjM1MTZoMTcuNTg5NWwxNi4yOTMyLDY3LjM1MzggMTkuNzk3Ni02Ny4zNTM4aDE0LjQzMDZsMTkuOTg4Myw2Ny4zNTM4IDE2LjI2MTctNjcuMzUzOGgxNy4xMTQ4em01OC44OTI1LDY1LjE0Mjd2LTE5LjYwODNoLTEwLjkyNDdjLTIyLjgzMDMsMC0zNC4yMjk3LDYuMTg4Ni0zNC4yMjk3LDE4LjU2NTkgMCw0LjUxNTMgMS43MDY0LDcuOTU4MSA1LjA4NDYsMTAuMzg4NSAzLjM3ODIsMi40MDAzIDcuODkzNSwzLjYwMDUgMTMuNTE0MywzLjYwMDUgMi41ODk2LDAgNS4xNDc3LS4zMTU0IDcuNzA1Ny0uODg0NyAyLjU1NjYtLjU5OTMgNS4wMi0xLjQyMSA3LjMyNTctMi41NTY2IDIuMzA0Mi0xLjEwNTUgNC40NTIyLTIuNDYzNCA2LjQwOTQtNC4wNzM3IDEuOTg4OC0xLjU3ODcgMy42OTM2LTMuNDExMiA1LjExNDYtNS40MzE2em0wLDE0LjI0MTNjLTguMjcyLDcuMzg4OC0xOC42MjksMTEuMDgyNC0zMS4xMDIzLDExLjA4MjQtNC4yNjI5LDAtOC4yMTA0LS41OTkzLTExLjg0MS0xLjgzMTFzLTYuODIxLTIuOTY4MS05LjUzNjgtNS4xNzc3Yy0yLjcxNDMtMi4yNDI2LTQuODMwNy00LjkyNjktNi4zNzc5LTguMDUyNy0xLjU0NzItMy4xNTc0LTIuMzA0Mi02LjYzMDItMi4zMDQyLTEwLjQ1MTYgMC0yMC42ODIzIDE3LjgwODgtMzEuMDA3NyA1My4zNjMzLTMxLjAwNzdoNy43OTg5YzAtMy44MjEzLS4xMjYyLTcuMTk5NS0uMzc4NS0xMC4xNjc3cy0xLjA0MjUtNS40MzE2LTIuMzY3My03LjQ1MTljLTEuMzI2My0xLjk5MDMtMy40NDI4LTMuNTM3NC02LjM0NzgtNC41Nzk5LTIuODczNS0xLjA0MS02Ljk0NzItMS41Nzg3LTEyLjE4OC0xLjU3ODctMTMuOTU3NCwwLTIxLjE1Nyw1LjA1My0yMS42NjE3LDE1LjA5NDVoLTE1LjgyYzEuMTA1NS0xOC4zMTUgMTMuOTI1OS0yNy40NzE4IDM4LjM5NjUtMjcuNDcxOCA1LjU1NzcsMCAxMC41NDc3LjQ3MzIgMTQuOTk5OSwxLjQ4NDEgNC40MjA3Ljk3OTQgOC4yMDg5LDIuNzE1OCAxMS4zMzYzLDUuMjA5MyAzLjA5NDMsMi40OTUgNS40OTMyLDUuOTM3OCA3LjE2NjUsMTAuMjYyMyAxLjY0MzMsNC4zNTc2IDIuNDk1LDkuODUyMiAyLjQ5NSwxNi41MTU1djU3LjA5aC0xNS42MzA4di04Ljk2NzV6IiBmaWxsPSIjMzMzMzMzIiBmaWxsLXJ1bGU9Im5vbnplcm8iPjwvcGF0aD4NCiAgPHBhdGggZD0ibTU4Mi44MDQsNTcuNDA4NWgtMS42NDkzdi03LjgwNzlsLTEuODAxLDcuODA3OWgtMS42NDkzbC0xLjc5NjUtNy44MDc5djcuODA3OWgtMS42NjQzdi05LjU1OTNoMi42Mjg3bDEuNjQ5Myw3LjM3MjMgMS42NjQzLTcuMzcyM2gyLjYxODJ2OS41NTkzem0tOS42NjE1LTcuOTU2NmgtMi42MDE2djcuOTU2NmgtMS43OTY1di03Ljk1NjZoLTIuNjAzMXYtMS42MDI3aDcuMDAxMnYxLjYwMjd6IiBmaWxsPSIjNGQ0OTQ4IiBmaWxsLXJ1bGU9Im5vbnplcm8iPjwvcGF0aD4NCjwvc3ZnPg0K") no-repeat; + .background-size(78px 25px); + line-height: 1px; + &:hover { + } +} + +.inline-block { + display: inline-block !important; +} + +.pre-line { + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; +} + +.mock { + .opacity(80); +} + +.balloon { + position: relative; + display: inline-block; + border: 1px solid #ddd; + .border-radius(4px); + padding: 8px 10px; + background: white; + .box-shadow(0 1px 1px rgba(0, 0, 0, 0.075)); + + &:after, &:before { + position: absolute; + width: 0; + height: 0; + border: solid transparent; + content: " "; + pointer-events: none; + } + + &:after { + border-width: 8px; + } + + &:before { + border-width: 9px; + } + + @media (min-width: 1px) { + margin-left: 10px; + &:after, &:before { + right: 100%; + top: 50%; + } + + &:after { + border-right-color: #fff; + margin-top: -8px; + } + + &:before { + border-right-color: #ddd; + margin-top: -9px; + } + + &.for-profile-picture { + min-height: (60 - @baseLineHeight); + + &:after, + &:before { + top: 30px; + } + } + } + + /* top arrow (disabled) */ + @media (max-width: 0px) { + margin-top: 9px; + + &:after, &:before { + bottom: 100%; + } + + &:after { + left: 50%; + border-bottom-color: #ffffff; + margin-left: -8px; + } + + &:before { + left: 50%; + border-bottom-color: #cccccc; + margin-left: -9px; + } + } +} + +/* Ellipse text overflow in input */ +input[type=text] { + text-overflow: ellipsis; +} + +body { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGAgMAAACdogfbAAAACVBMVEX////8/Pz39/eV2hHsAAAAGUlEQVR4XiXCAQkAAADCsOe0nz3lOEZwfQMXNwH/qkrDbwAAAABJRU5ErkJggg%3D%3D'); + .background-size(3px 3px); +} + +.inline-flag { + display: inline-block; + margin-bottom: 0; + padding-left: 5px; + vertical-align: middle; +} + +.shy { + color: @grayLight; +} + +footer { + li { + display: inline-block; + } + li + li { + padding: 0 5px; + border-left: 1px solid @grayLight; + } +} + +/* Twitter Bootstrap FIX */ +@media(max-width: 979px) { + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after, + .nav-collapse li.dropdown > .dropdown-toggle .caret { + display: none !important; + } +} + +.errorText { + color: @errorText; +} diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less new file mode 100644 index 000000000..6075d129d --- /dev/null +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -0,0 +1,155 @@ +/* ========================================================================== + Bootstrap and overrides + ========================================================================== */ + +@import "twitter/bootstrap/bootstrap"; + +body { + padding-top: 60px; + padding-bottom: @baseLineHeight * 1.5; +} + +@import "twitter/bootstrap/responsive"; + +@blueFacebook: #3b5998; + +/* Icons + ========================================================================== */ + +@fontAwesomeEotPath: 'fontawesome-webfont.eot'; +@fontAwesomeWoffPath: 'fontawesome-webfont.woff'; +@fontAwesomeTtfPath: 'fontawesome-webfont.ttf'; +@fontAwesomeSvgzPath: 'fontawesome-webfont.svgz'; +@fontAwesomeSvgPath: 'fontawesome-webfont.svg'; +@import "fontawesome"; + +/* Overriding default variables + ========================================================================== */ + +@sansFontFamily: "Oxygen", "Helvetica Neue", Helvetica, Arial, sans-serif; + +@linkColor: lighten(#16576b, 10%); + +@navbarBackground: rgba(22, 87, 107, 0.7); /*#16576b*/ +@navbarBackgroundHighlight: lighten(@navbarBackground, 10%); + +@navbarText: @grayLighter; +@navbarLinkColor: @grayLighter; +@navbarLinkColorHover: @white; +@navbarLinkColorActive: @white; + +/* ========================================================================== + Google Places Autocompletion + ========================================================================== */ + +.pac-container { + z-index: @zindexFixedNavbar + 1 !important; + min-width: 200px; + .border-radius(4px); + margin-top: 2px; // give it some space to breathe + + .pac-item { + padding: 3px 15px; + line-height: 18px; + &:hover { + color: @white; + background-color: @linkColor; + } + } + + &::after { + padding-right: 3px; + } +} + +/* ========================================================================== + Carousel fade effect + ========================================================================== */ + +&.carousel-fade { + .item { + .transition(opacity 1s ease-in-out); + } + .active { + &.left, &.right { + z-index: 2; + left: 0; + .opacity(0); + } + } + .next, .prev { + z-index: 1; + left: 0; + } + .carousel-control { + z-index: 3; + } +} + +/* ========================================================================== + Global styles + ========================================================================== */ + +.legend-like { + display: block; + width: 100%; + padding: 0; + margin-bottom: @baseLineHeight / 2; + font-weight: normal; + border: 0; + border-bottom: 1px solid #eee; + + small { + font-size: @baseLineHeight * .75; + color: @grayLight; + } +} + +.tooltip { + z-index: 100000; +} + +.facebook-bg { + background: @blueFacebook; +} + +.facebook-color { + color: @blueFacebook; +} + +.btn-facebook { + color: @white; + .buttonBackground(lighten(@blueFacebook, 10%), @blueFacebook); + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + + &:hover { + color: @white; + } +} + +/* Icon Button Fix */ +.btn { + i.icon-large[class^=icon-] { + margin-right: 3px; + } +} + +.spacer { + height: @baseLineHeight; +} + +/* Thumbnail background fix */ +.thumbnail { + background: @white; +} + +/* Lighter modal backdrop */ +.modal-backdrop, +.modal-backdrop.fade.in { + .opacity(60); +} + +.modal-header { + background-color: #f5f5f5; + .border-radius(6px 6px 0 0); +} diff --git a/app/assets/stylesheets/conversation.css.less b/app/assets/stylesheets/conversation.css.less new file mode 100644 index 000000000..bd95a2a06 --- /dev/null +++ b/app/assets/stylesheets/conversation.css.less @@ -0,0 +1,42 @@ +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +.conversation-title { + font-size: 24px; + font-weight: normal; +} + +.conversation { + li { + display: table; + width: 100%; + padding: 12px 0; + .transition(background 4s ease-out); + + &.alternate { + background: darken(@white, 5%); + } + + &.unread { + background: @warningBackground; + } + } + + li + li { + border-top: 1px solid #ccc; + } + + .message-avatar, + .message { + display: table-cell; + vertical-align: top; + } + + .message-avatar { + width: 60px; + } + + .message-body { + margin-bottom: 0; + } +} diff --git a/app/assets/stylesheets/dashboard.css.less b/app/assets/stylesheets/dashboard.css.less new file mode 100644 index 000000000..27f8eba79 --- /dev/null +++ b/app/assets/stylesheets/dashboard.css.less @@ -0,0 +1,54 @@ +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +.dashboard-title { + margin-bottom: @baseLineHeight; + font-size: 40px; + line-height: 44px; + text-align: center; + color: @white; + text-shadow: 2px 2px 2px @black; + .opacity(95); +} + +#hero-user, +#hero-itineraries { + position: relative; + padding: 30px 20px; + + @media (max-width: 979px) and (min-width: 768px) { + } + + @media (min-width: 980px) { + } + + p { + .opacity(95); + margin-left: 30px; + margin-right: 30px; + } + + .attribution { + position: absolute; + bottom: 5px; + right: 10px; + font-size: 10px; + color: rgba(255, 255, 255, 0.5); + cursor: default; + .user-select(none); + } +} + +#hero-itineraries { + /* background: url('/assets/road.jpg'); */ /* CC 3.0 http://www.flickr.com/photos/joanet/2994421437/ */ + background: #336688 url('/assets/dash_road.jpg'); /* CC 3.0 http://www.flickr.com/photos/xjrlokix/2542767294/ */ + .background-size(140% auto); + background-position: center bottom; +} + +#hero-user { + /* background: url('/assets/blank-black-chalkboard.jpg'); */ + background: #222222 url('/assets/dash_dashboard.jpg'); /* http://www.flickr.com/photos/hine/316920241/ */ + .background-size(140% auto); + background-position: center; +} diff --git a/app/assets/stylesheets/flags.css.less b/app/assets/stylesheets/flags.css.less new file mode 100644 index 000000000..f9154bb7b --- /dev/null +++ b/app/assets/stylesheets/flags.css.less @@ -0,0 +1,266 @@ +@import "twitter/bootstrap/mixins"; +@import "twitter/bootstrap/variables"; + +[class^="flag-"] { + display: inline-block; + .size(11px, 16px); + .border-radius(2px); + margin-right: 2px; + margin-top: 2px; + background: @grayLighter url('/assets/flags.png') no-repeat; +} + +/* Swiss patch */ +.flag-ch { + .size(11px, 11px); +} + +.flag-ad { background-position: -16px 0 } +.flag-ae { background-position: -32px 0 } +.flag-af { background-position: -48px 0 } +.flag-ag { background-position: -64px 0 } +.flag-ai { background-position: -80px 0 } +.flag-al { background-position: -96px 0 } +.flag-am { background-position: -112px 0 } +.flag-an { background-position: -128px 0 } +.flag-ao { background-position: -144px 0 } +.flag-ar { background-position: -160px 0 } +.flag-as { background-position: -176px 0 } +.flag-at { background-position: -192px 0 } +.flag-au { background-position: -208px 0 } +.flag-aw { background-position: -224px 0 } +.flag-az { background-position: -240px 0 } +.flag-ba { background-position: 0 -11px } +.flag-bb { background-position: -16px -11px } +.flag-bd { background-position: -32px -11px } +.flag-be { background-position: -48px -11px } +.flag-bf { background-position: -64px -11px } +.flag-bg { background-position: -80px -11px } +.flag-bh { background-position: -96px -11px } +.flag-bi { background-position: -112px -11px } +.flag-bj { background-position: -128px -11px } +.flag-bm { background-position: -144px -11px } +.flag-bn { background-position: -160px -11px } +.flag-bo { background-position: -176px -11px } +.flag-br { background-position: -192px -11px } +.flag-bs { background-position: -208px -11px } +.flag-bt { background-position: -224px -11px } +.flag-bv { background-position: -240px -11px } +.flag-bw { background-position: 0 -22px } +.flag-by { background-position: -16px -22px } +.flag-bz { background-position: -32px -22px } +.flag-ca { background-position: -48px -22px } +.flag-catalonia { background-position: -64px -22px } +.flag-cd { background-position: -80px -22px } +.flag-cf { background-position: -96px -22px } +.flag-cg { background-position: -112px -22px } +.flag-ch { background-position: -128px -22px } +.flag-ci { background-position: -144px -22px } +.flag-ck { background-position: -160px -22px } +.flag-cl { background-position: -176px -22px } +.flag-cm { background-position: -192px -22px } +.flag-cn { background-position: -208px -22px } +.flag-co { background-position: -224px -22px } +.flag-cr { background-position: -240px -22px } +.flag-cu { background-position: 0 -33px } +.flag-cv { background-position: -16px -33px } +.flag-cw { background-position: -32px -33px } +.flag-cy { background-position: -48px -33px } +.flag-cz { background-position: -64px -33px } +.flag-de { background-position: -80px -33px } +.flag-dj { background-position: -96px -33px } +.flag-dk { background-position: -112px -33px } +.flag-dm { background-position: -128px -33px } +.flag-do { background-position: -144px -33px } +.flag-dz { background-position: -160px -33px } +.flag-ec { background-position: -176px -33px } +.flag-ee { background-position: -192px -33px } +.flag-eg { background-position: -208px -33px } +.flag-eh { background-position: -224px -33px } +.flag-england { background-position: -240px -33px } +.flag-er { background-position: 0 -44px } +.flag-es { background-position: -16px -44px } +.flag-et { background-position: -32px -44px } +.flag-eu { background-position: -48px -44px } +.flag-fi { background-position: -64px -44px } +.flag-fj { background-position: -80px -44px } +.flag-fk { background-position: -96px -44px } +.flag-fm { background-position: -112px -44px } +.flag-fo { background-position: -128px -44px } +.flag-fr { background-position: -144px -44px } +.flag-ga { background-position: -160px -44px } +.flag-gb { background-position: -176px -44px } +.flag-gd { background-position: -192px -44px } +.flag-ge { background-position: -208px -44px } +.flag-gf { background-position: -224px -44px } +.flag-gg { background-position: -240px -44px } +.flag-gh { background-position: 0 -55px } +.flag-gi { background-position: -16px -55px } +.flag-gl { background-position: -32px -55px } +.flag-gm { background-position: -48px -55px } +.flag-gn { background-position: -64px -55px } +.flag-gp { background-position: -80px -55px } +.flag-gq { background-position: -96px -55px } +.flag-gr { background-position: -112px -55px } +.flag-gs { background-position: -128px -55px } +.flag-gt { background-position: -144px -55px } +.flag-gu { background-position: -160px -55px } +.flag-gw { background-position: -176px -55px } +.flag-gy { background-position: -192px -55px } +.flag-hk { background-position: -208px -55px } +.flag-hm { background-position: -224px -55px } +.flag-hn { background-position: -240px -55px } +.flag-hr { background-position: 0 -66px } +.flag-ht { background-position: -16px -66px } +.flag-hu { background-position: -32px -66px } +.flag-id { background-position: -48px -66px } +.flag-ie { background-position: -64px -66px } +.flag-il { background-position: -80px -66px } +.flag-im { background-position: -96px -66px } +.flag-in { background-position: -112px -66px } +.flag-io { background-position: -128px -66px } +.flag-iq { background-position: -144px -66px } +.flag-ir { background-position: -160px -66px } +.flag-is { background-position: -176px -66px } +.flag-it { background-position: -192px -66px } +.flag-je { background-position: -208px -66px } +.flag-jm { background-position: -224px -66px } +.flag-jo { background-position: -240px -66px } +.flag-jp { background-position: 0 -77px } +.flag-ke { background-position: -16px -77px } +.flag-kg { background-position: -32px -77px } +.flag-kh { background-position: -48px -77px } +.flag-ki { background-position: -64px -77px } +.flag-km { background-position: -80px -77px } +.flag-kn { background-position: -96px -77px } +.flag-kp { background-position: -112px -77px } +.flag-kr { background-position: -128px -77px } +.flag-kurdistan { background-position: -144px -77px } +.flag-kw { background-position: -160px -77px } +.flag-ky { background-position: -176px -77px } +.flag-kz { background-position: -192px -77px } +.flag-la { background-position: -208px -77px } +.flag-lb { background-position: -224px -77px } +.flag-lc { background-position: -240px -77px } +.flag-li { background-position: 0 -88px } +.flag-lk { background-position: -16px -88px } +.flag-lr { background-position: -32px -88px } +.flag-ls { background-position: -48px -88px } +.flag-lt { background-position: -64px -88px } +.flag-lu { background-position: -80px -88px } +.flag-lv { background-position: -96px -88px } +.flag-ly { background-position: -112px -88px } +.flag-ma { background-position: -128px -88px } +.flag-mc { background-position: -144px -88px } +.flag-md { background-position: -160px -88px } +.flag-me { background-position: -176px -88px } +.flag-mg { background-position: -192px -88px } +.flag-mh { background-position: -208px -88px } +.flag-mk { background-position: -224px -88px } +.flag-ml { background-position: -240px -88px } +.flag-mm { background-position: 0 -99px } +.flag-mn { background-position: -16px -99px } +.flag-mo { background-position: -32px -99px } +.flag-mp { background-position: -48px -99px } +.flag-mq { background-position: -64px -99px } +.flag-mr { background-position: -80px -99px } +.flag-ms { background-position: -96px -99px } +.flag-mt { background-position: -112px -99px } +.flag-mu { background-position: -128px -99px } +.flag-mv { background-position: -144px -99px } +.flag-mw { background-position: -160px -99px } +.flag-mx { background-position: -176px -99px } +.flag-my { background-position: -192px -99px } +.flag-mz { background-position: -208px -99px } +.flag-na { background-position: -224px -99px } +.flag-nc { background-position: -240px -99px } +.flag-ne { background-position: 0 -110px } +.flag-nf { background-position: -16px -110px } +.flag-ng { background-position: -32px -110px } +.flag-ni { background-position: -48px -110px } +.flag-nl { background-position: -64px -110px } +.flag-no { background-position: -80px -110px } +.flag-np { background-position: -96px -110px } +.flag-nr { background-position: -112px -110px } +.flag-nu { background-position: -128px -110px } +.flag-nz { background-position: -144px -110px } +.flag-om { background-position: -160px -110px } +.flag-pa { background-position: -176px -110px } +.flag-pe { background-position: -192px -110px } +.flag-pf { background-position: -208px -110px } +.flag-pg { background-position: -224px -110px } +.flag-ph { background-position: -240px -110px } +.flag-pk { background-position: 0 -121px } +.flag-pl { background-position: -16px -121px } +.flag-pm { background-position: -32px -121px } +.flag-pn { background-position: -48px -121px } +.flag-pr { background-position: -64px -121px } +.flag-ps { background-position: -80px -121px } +.flag-pt { background-position: -96px -121px } +.flag-pw { background-position: -112px -121px } +.flag-py { background-position: -128px -121px } +.flag-qa { background-position: -144px -121px } +.flag-re { background-position: -160px -121px } +.flag-ro { background-position: -176px -121px } +.flag-rs { background-position: -192px -121px } +.flag-ru { background-position: -208px -121px } +.flag-rw { background-position: -224px -121px } +.flag-sa { background-position: -240px -121px } +.flag-sb { background-position: 0 -132px } +.flag-sc { background-position: -16px -132px } +.flag-scotland { background-position: -32px -132px } +.flag-sd { background-position: -48px -132px } +.flag-se { background-position: -64px -132px } +.flag-sg { background-position: -80px -132px } +.flag-sh { background-position: -96px -132px } +.flag-si { background-position: -112px -132px } +.flag-sk { background-position: -128px -132px } +.flag-sl { background-position: -144px -132px } +.flag-sm { background-position: -160px -132px } +.flag-sn { background-position: -176px -132px } +.flag-so { background-position: -192px -132px } +.flag-somaliland { background-position: -208px -132px } +.flag-sr { background-position: -224px -132px } +.flag-ss { background-position: -240px -132px } +.flag-st { background-position: 0 -143px } +.flag-sv { background-position: -16px -143px } +.flag-sy { background-position: -32px -143px } +.flag-sz { background-position: -48px -143px } +.flag-tc { background-position: -64px -143px } +.flag-td { background-position: -80px -143px } +.flag-tf { background-position: -96px -143px } +.flag-tg { background-position: -112px -143px } +.flag-th { background-position: -128px -143px } +.flag-tj { background-position: -144px -143px } +.flag-tk { background-position: -160px -143px } +.flag-tl { background-position: -176px -143px } +.flag-tm { background-position: -192px -143px } +.flag-tn { background-position: -208px -143px } +.flag-to { background-position: -224px -143px } +.flag-tr { background-position: -240px -143px } +.flag-tt { background-position: 0 -154px } +.flag-tv { background-position: -16px -154px } +.flag-tw { background-position: -32px -154px } +.flag-tz { background-position: -48px -154px } +.flag-ua { background-position: -64px -154px } +.flag-ug { background-position: -80px -154px } +.flag-um { background-position: -96px -154px } +.flag-us { background-position: -112px -154px } +.flag-uy { background-position: -128px -154px } +.flag-uz { background-position: -144px -154px } +.flag-va { background-position: -160px -154px } +.flag-vc { background-position: -176px -154px } +.flag-ve { background-position: -192px -154px } +.flag-vg { background-position: -208px -154px } +.flag-vi { background-position: -224px -154px } +.flag-vn { background-position: -240px -154px } +.flag-vu { background-position: 0 -165px } +.flag-wales { background-position: -16px -165px } +.flag-wf { background-position: -32px -165px } +.flag-ws { background-position: -48px -165px } +.flag-ye { background-position: -64px -165px } +.flag-yt { background-position: -80px -165px } +.flag-za { background-position: -96px -165px } +.flag-zanzibar { background-position: -112px -165px } +.flag-zm { background-position: -128px -165px } +.flag-zw { background-position: -144px -165px } diff --git a/app/assets/stylesheets/itineraries.css.less b/app/assets/stylesheets/itineraries.css.less new file mode 100644 index 000000000..b7f5963cb --- /dev/null +++ b/app/assets/stylesheets/itineraries.css.less @@ -0,0 +1,111 @@ +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +.itinerary-step { + border-bottom: 2px solid @grayDark; + &:not(.active) { + border-color: @grayLight; + color: @grayLight; + } + &.done { + border-color: @successText; + color: @successText; + } +} + +select.date-time { + width: auto; +} + +#table-daily-itinerary th { + text-align: left; +} + +.profile-picture-cell { + width: 60px; +} + +#validation-error { + color: @errorText; +} + +#itinerary-preview { + background: @warningBackground; +} + +.itinerary-thumb-stroke { + border: 0; + border-top: 3px solid #ff0000; +} + +.itinerary-thumbnail { + display: block; + padding: 4px; + border: 1px solid #ddd; + margin-bottom: @baseLineHeight; + .border-radius(4px); + .box-shadow(0 1px 1px rgba(0,0,0,.075)); + background: @white; + + table { + margin-bottom: 2px; + } + + td { + padding-top: 3px; + vertical-align: top; + } + + td.user-profile-picture { + width: 50px; + padding-right: 5px; + } + + .it-title, + .it-user { + display: block; + height: @baseLineHeight; + overflow: hidden; + } + + .btn-toolbar { + text-align: right; + margin: 0; + } +} + +#itineraries-spinner { + margin-left: 10px; +} + +#search-form-advanced { + margin-bottom: 10px; + + label, + input, + button, + select, + textarea { + font-size: 12px; + line-height: 14px; + } + + select, + input[type="file"] { + height: 26px; + } + + .control-group { + margin-bottom: 0; + } + + select, textarea, input, .uneditable-input { + width: auto !important; + padding: 3px 5px; + margin-bottom: 4px; + } + + td.select-field { + padding-left: 5px; + } +} diff --git a/app/assets/stylesheets/map-markers.css.scss b/app/assets/stylesheets/map-markers.css.scss new file mode 100644 index 000000000..05787e64b --- /dev/null +++ b/app/assets/stylesheets/map-markers.css.scss @@ -0,0 +1,2 @@ +@import "markers/*.png"; +@include all-markers-sprites; diff --git a/app/assets/stylesheets/maps.css.less b/app/assets/stylesheets/maps.css.less new file mode 100644 index 000000000..01e77a43b --- /dev/null +++ b/app/assets/stylesheets/maps.css.less @@ -0,0 +1,108 @@ +@import "twitter/bootstrap/mixins"; +@import "twitter/bootstrap/variables"; + +.google-maps-fix { + label { + display:inline; + width: auto; + } + + img { + max-width: none; + } + .box-sizing(border-box); +} + +#index-itineraries-map, +#new-itinerary-map { + .size(300px, 100%); + border: 1px solid #ddd; +} + +#error, +#result { + display: none; +} + +.gmaps-popup { + height: 100px; //needed by firefox +} + +.gmaps-popup-image, +.gmaps-popup-text { + display: table-cell; +} + +.gmaps-popup-image { + padding-right: 10px; +} + +.gmaps-popup-image img { + display: block; + height: 50px; + width: 50px; +} + +.gmaps-popup-text { + vertical-align: top; + font-size: 12px; +} + +.gmaps-popup-p-title { + margin: 0 0 2px; + font-weight: bold; + font-size: 16px; +} + +.gmaps-popup-p-content { + margin: 0; + color: #666; +} + +.gmaps-popup-div-details { + margin: 10px 0 0; + span { + display: inline-block; + margin-right: 12px; + text-align: center; + } +} + +.arrow_box { + display: block; + border: 1px solid @grayLight; + background: @white; + .border-radius(4px); + .box-shadow(2px 2px 1px rgba(0, 0, 0, 0.2)); + cursor: pointer; + + img { + position: absolute; + top: 3px; + left: 3px; + } +} + +.arrow_box:after, .arrow_box:before { + position: absolute; + top: 100%; + height: 0; + width: 0; + border: solid transparent; + content: " "; + pointer-events: none; +} + +.arrow_box:after { + left: 50%; + margin-left: -6px; + border-top-color: @white; + border-width: 6px; +} + +.arrow_box:before { + border-top-color: @grayLight; + border-width: 7px; + left: 50%; + margin-left: -7px; +} diff --git a/app/assets/stylesheets/navbar.css.less b/app/assets/stylesheets/navbar.css.less new file mode 100644 index 000000000..9b789fb97 --- /dev/null +++ b/app/assets/stylesheets/navbar.css.less @@ -0,0 +1,91 @@ +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +.navbar { + .divider-vertical { + .opacity(70); + } + + .brand { + font-family: "Pacifico"; + color: @grayLighter; + font-size: 20px; + line-height: 20px; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + .transition(text-shadow .4s ease-out); + + &:hover { + color: white; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25), + 0 0 25px rgba(222, 231, 238, 0.8); + } + } +} + +.brand-logo { + position: relative; + display: inline-block; + top: 4px; + .size(15px, 38px); + vertical-align: top; +} + +#user-navbar-info { + a { + display: inline-block; + } + + .navbar-profile-picture { + position: relative; + border: 1px solid @grayLighter; + padding: 0; + .square(25px); + + &:hover { + border: 1px solid @white; + } + } +} + +@media (min-width: 768px) { + #user-navbar-info { + .navbar-profile-picture { + top: -1px; + } + + .navbar-profile-name { + padding-left: 5px; + } + } +} + +@media (max-width: 767px) { + #user-navbar-info { + .navbar-profile-picture { + top: 3px; + .square(30px); + } + } +} + +.notifications { + position: relative; + line-height: 17px; + + a { + padding: 10px 10px 9px !important; + } + + i { + margin-top: 2px; + font-size: 20px; + } + + .count { + position: absolute; + top: 2px; + right: 4px; + height: 12px; + background: rgba(205, 0, 0, 0.8); + } +} diff --git a/app/assets/stylesheets/notifications.css.less b/app/assets/stylesheets/notifications.css.less new file mode 100644 index 000000000..34fb1c177 --- /dev/null +++ b/app/assets/stylesheets/notifications.css.less @@ -0,0 +1,22 @@ +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +@background: lighten(@successBackground, 5%); + +.notification-unread { + background-color: @background; +} + +.table-striped { + tbody { + tr:nth-child(odd) td.notification-unread { + background-color: darken(@background, 1%); + } + } +} + +.table { + tbody tr:hover td.notification-unread { + background-color: darken(@background, 3%); + } +} diff --git a/app/assets/stylesheets/pages.css.less b/app/assets/stylesheets/pages.css.less new file mode 100644 index 000000000..10214fbcb --- /dev/null +++ b/app/assets/stylesheets/pages.css.less @@ -0,0 +1,58 @@ +@import "twitter/bootstrap/mixins"; + +#home-hero { + background-color: #dee7ee; +} + +.home-hero-logo { + position: relative; + display: inline-block; + top: 7px; + .size(50px, 127px); + vertical-align: top; +} + +p.signin-terms { + line-height: 18px; + font-size: 12px; +} + +.fb-facepile { + width: 100% !important; + margin-bottom: 18px; + + & > span, + iframe { + width: 100% !important; + /* NOTE: 19px right offset, consider fixed width */ + } +} + +#home-picture { + max-width: 300px; + padding: 0 20px 40px 0; + .rotate(-3deg); + + .thumbnail { + background-position: center; + .box-shadow(3px 3px 3px rgba(0, 0, 0, 0.25)); + } + + img { + .transition(opacity .2s ease-in); + .opacity(80); + &:hover { + .opacity(100); + } + } +} + +.why-facebook { + margin-left: 5px; +} + +.home-h2 { + margin: 0; + font-size: 26px; + line-height: 34px; +} diff --git a/app/assets/stylesheets/references.css.less b/app/assets/stylesheets/references.css.less new file mode 100644 index 000000000..be98c80c1 --- /dev/null +++ b/app/assets/stylesheets/references.css.less @@ -0,0 +1,7 @@ +.not-relevant-for-me td { + background-color: #eee !important; +} + +.not-relevant-for-other td { + background-color: #dfd !important; +} diff --git a/app/assets/stylesheets/users.css.less b/app/assets/stylesheets/users.css.less new file mode 100644 index 000000000..529d92ed2 --- /dev/null +++ b/app/assets/stylesheets/users.css.less @@ -0,0 +1,153 @@ +@import "twitter/bootstrap/variables"; +@import "twitter/bootstrap/mixins"; + +@media (min-width: 768px) { + .profilenav-fixed { + z-index: 1; + position: fixed; + top: 10px; + background: @white; + .transition(top .2s linear); + } + + .profilenav-helper { + display: block; + } +} + +@media (min-width: 980px) { + .profilenav-fixed { + top: 60px; + } +} + +#flag[class^=flag-] { + margin-right: 5px; +} + +ul.tag-list { + line-height: 26px; + + li { + display: inline-block; + .ie7-inline-block(); + margin-right: 6px; + border: 1px solid @btnBorder; + border-bottom-color: darken(@btnBorder, 10%); + padding: 2px 6px; + font-size: 12px; + line-height: 14px; + white-space: pre; + text-align: center; + vertical-align: middle; + color: @grayDark; + text-shadow: 0 1px 1px rgba(255,255,255,.75); + .buttonBackground(@btnBackground, @btnBackgroundHighlight); + .border-radius(4px); + .ie7-restore-left-whitespace(); // Give IE7 some love + /* .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)"); */ + cursor: default; + } + + li.common, + li.description, + li.description-facebook { + color: @white; + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } + + li.common { + .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight); + } + + li.description { + .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); + } + + li.description-facebook { + @blueFacebook: #3b5998; + .buttonBackground(@blueFacebook, darken(@blueFacebook, 10%)); + } +} + +dl.tag-list { + font-size: 12px; + + dt, + dd { + font-weight: normal; + line-height: 26px; + margin-bottom: 13px; + } + + a { + position: relative; + top: 1px; + } + + span { + display: inline-block; + .ie7-inline-block(); + border: 1px solid @btnBorder; + /* border-bottom-color: darken(@btnBorder, 10%); */ + padding: 2px 6px; + line-height: 14px; + white-space: pre; + text-align: center; + vertical-align: middle; + .buttonBackground(@btnBackground, @btnBackgroundHighlight); + .border-radius(4px); + .ie7-restore-left-whitespace(); // Give IE7 some love + /* .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)"); */ + cursor: default; + } + + dd > span { + margin-right: 6px; + } + + dd.friends { + vertical-align: top; + line-height: 31px; + margin-bottom: 8px; + + span { + margin-bottom: 5px; + } + + img { + margin-right: 5px; + } + + a { + top: -2px; + } + } + + dt > span, + .common, + .description, + .description-facebook { + color: @white; + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } + + .common { + .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight); + } + + dt > span, + .description { + .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); + } + + .description-facebook { + @blueFacebook: #3b5998; + .buttonBackground(@blueFacebook, darken(@blueFacebook, 10%)); + } + + dd > span:not(.common) { + color: @grayDark; + text-shadow: 0 1px 1px rgba(255,255,255,.75); + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..88ca6de41 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,56 @@ +class ApplicationController < ActionController::Base + protect_from_forgery + + before_filter :require_login + before_filter :set_locale + before_filter :set_user_time_zone + before_filter :check_banned, except: [ :banned ] + before_filter :check_admin, only: [ :index ] # whitelist approach + + helper_method :current_user, :logged_in? + +protected + + def set_user_time_zone + Time.zone = current_user.time_zone if logged_in? + end + + def set_locale + # TODO think about optimizing this + I18n.locale = \ + (params[:locale].to_sym if params[:locale].present? && I18n.available_locales.include?(params[:locale].to_sym)) \ + || (current_user.locale.to_sym if logged_in? && current_user.locale && I18n.available_locales.include?(current_user.locale.to_sym)) \ + || (request.preferred_language_from(I18n.available_locales) if request.preferred_language_from(I18n.available_locales)) \ + || (request.compatible_language_from(I18n.available_locales) if request.compatible_language_from(I18n.available_locales)) + end + + def require_login + if current_user.nil? + redirect_to root_path, flash: { error: t('flash.error.not_authenticated') } + end + end + + def check_admin + unless current_user && current_user.admin? + redirect_to root_path, flash: { error: t('flash.error.not_allowed') } + end + end + + def check_banned + if current_user && current_user.banned? + redirect_to :banned + end + end + +private + + def current_user + @current_user ||= User.find(session[:user_id]) if session[:user_id] + rescue + session[:user_id] = nil + end + + def logged_in? + !!current_user + end +end diff --git a/app/controllers/beta_invites_controller.rb b/app/controllers/beta_invites_controller.rb new file mode 100644 index 000000000..b5d20ed04 --- /dev/null +++ b/app/controllers/beta_invites_controller.rb @@ -0,0 +1,29 @@ +class BetaInvitesController < ApplicationController + + before_filter :check_admin + + def index + @beta_invites = BetaInvite.all + end + + def new + @beta_invite = BetaInvite.new + end + + def create + @beta_invite = BetaInvite.new(params[:beta_invite]) + + if @beta_invite.save + redirect_to beta_invites_path, notice: 'Beta invite was successfully created.' + else + render :new + end + end + + def destroy + @beta_invite = BetaInvite.find(params[:id]) + @beta_invite.destroy + redirect_to beta_invites_path + end + +end diff --git a/app/controllers/conversations_controller.rb b/app/controllers/conversations_controller.rb new file mode 100644 index 000000000..a2d5d24e8 --- /dev/null +++ b/app/controllers/conversations_controller.rb @@ -0,0 +1,68 @@ +class ConversationsController < ApplicationController + + skip_before_filter :check_admin, only: [:index] + + before_filter :check_user, only: :show + before_filter :clean_message_params, only: [:create, :update] + + after_filter :mark_as_read, only: [:show] + + def new + @itinerary = Itinerary.includes(:user).find(params[:itinerary_id]) + @conversation = Conversation.new + end + + def create + @itinerary = Itinerary.find(params[:itinerary_id]) + @conversation = @itinerary.conversations.build(params[:conversation]) + @conversation.messages.build(params[:conversation][:message]) + @conversation.users << current_user + @conversation.users << @itinerary.user + if @conversation.save + redirect_to conversation_path(@conversation) + else + flash.now[:error] = @conversation.errors.full_messages + render :new + end + end + + def update + @conversation = Conversation.find(params[:id]) + @conversation.messages.build(params[:conversation][:message]) + if @conversation.save + redirect_to conversation_path(@conversation) + else + flash.now[:error] = @conversation.errors.full_messages + @conversation.reload + @itinerary = Itinerary.find(@conversation.conversable_id) + render :show + end + end + + def index + @conversations = current_user.conversations + end + + def show + @conversation = Conversation.find(params[:id]) + @itinerary = Itinerary.find(@conversation.conversable_id) + end + +protected + + def check_user + #@conversation = Conversation.find(params[:id]) + #redirect_to root_path, flash: { error: t('flash.error.not_authenticated') } unless current_user.conversations.include?(@conversation) + end + +private + + def clean_message_params + params[:conversation][:message]['sender'] = current_user + params[:conversation][:message]['unread'] = nil + end + + def mark_as_read + @conversation.mark_as_read(current_user) + end +end diff --git a/app/controllers/feedbacks_controller.rb b/app/controllers/feedbacks_controller.rb new file mode 100644 index 000000000..be894a465 --- /dev/null +++ b/app/controllers/feedbacks_controller.rb @@ -0,0 +1,46 @@ +class FeedbacksController < ApplicationController + + skip_before_filter :check_admin, only: [:index] + + def index + @feedbacks = Feedback.includes(:user).all.desc(:updated_at) + @feedbacks = @feedbacks.where(:status.ne => "fixed") if params[:hide_fixed] + @url = request.env['HTTP_REFERER'] + end + + def show + @feedback = Feedback.find(params[:id]) + end + + def new + @feedback = Feedback.new(url: params[:url]) + end + + def create + attrs = params[:feedback] + attrs.delete("status") unless current_user.admin? + @feedback = Feedback.new(attrs) + @feedback.user = current_user + if @feedback.save + redirect_to feedbacks_path, flash: { success: t('flash.feedback.success.create') } + else + flash.now[:error] = @feedback.errors.full_messages + render :new + end + end + + def edit + @feedback = Feedback.find(params[:id]) + end + + def update + @feedback = Feedback.find(params[:id]) + + if @feedback.update_attributes(params[:feedback]) + redirect_to feedbacks_path, flash: { success: t('flash.feedback.success.update') } + else + flash.now[:error] = @feedback.errors.full_messages + render :edit + end + end +end diff --git a/app/controllers/itineraries/build_controller.rb b/app/controllers/itineraries/build_controller.rb new file mode 100644 index 000000000..98ad96b62 --- /dev/null +++ b/app/controllers/itineraries/build_controller.rb @@ -0,0 +1,23 @@ +=begin +class Itineraries::BuildController < ApplicationController + include Wicked::Wizard + + steps :details, :confirm_and_share + + def show + @itinerary = Itinerary.find(params[:itinerary_id]) + render_wizard + end + + def update + @itinerary = Itinerary.find(params[:itinerary_id]) + @itinerary.update_attributes(params[:itinerary]) + render_wizard @itinerary + end + + def create + @itinerary = Itinerary.create + redirect_to wizard_path(steps.first, itinerary_id: @itinerary.id) + end +end +=end diff --git a/app/controllers/itineraries_controller.rb b/app/controllers/itineraries_controller.rb new file mode 100644 index 000000000..184871c9c --- /dev/null +++ b/app/controllers/itineraries_controller.rb @@ -0,0 +1,72 @@ +class ItinerariesController < ApplicationController + + skip_before_filter :check_admin, only: [:index] + skip_before_filter :require_login, only: [:index, :show, :search] + + before_filter :check_permissions + # TODO proper edit methods + + def new + @itinerary = Itinerary.new + end + + def index + #@itineraries = Itinerary.includes(:user).all + end + + def mine + @itineraries = current_user.itineraries + end + + def show + @itinerary = Itinerary.find(params[:id]) + end + + def create + @itinerary = Itinerary.build_with_route_json_object(params[:itinerary], current_user) + if @itinerary.save + redirect_to itinerary_path(@itinerary) + else + render :new + end + end + + def edit + end + + def update + end + + def destroy + @itinerary = current_user.itineraries.find(params[:id]) + if @itinerary.destroy + redirect_to my_itineraries_path, flash: { success: t('flash.itinerary.success.destroy') } + else + redirect_to my_itineraries_path, flash: { error: t('flash.itinerary.error.destroy') } + end + rescue + redirect_to my_itineraries_path, flash: { error: t('flash.itinerary.error.destroy') } + end + + def search + respond_to do |format| + format.json do + if request.xhr? + @itineraries = Itinerary.search(params[:itinerary_search]) + else + redirect_to root_path + end + end + format.html do + render(layout: false , json: {success: true, + data: Itinerary.all.as_json(except: [:deleted_at, + :overview_path]) }) + end + end + end + +protected + + def check_permissions + end +end diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb new file mode 100644 index 000000000..a68d54639 --- /dev/null +++ b/app/controllers/messages_controller.rb @@ -0,0 +1,19 @@ +class MessagesController < ApplicationController + + before_filter :check_user + + def create + @message = conversation.messages.create(params[:message].merge(sender: current_user)) + redirect_to conversation_path(conversation) + end + + protected + + def conversation + @conversation ||= Conversation.find(params[:conversation_id]) + end + + def check_user + redirect_back_or_to root_path, flash: { error: t('flash.error.not_authenticated') } unless current_user.conversations.include?(conversation) + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 000000000..adb2c6a63 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,15 @@ +class NotificationsController < ApplicationController + + skip_before_filter :check_admin, only: [:index] + + after_filter :consume_notifications, only: :index + + def index + @notifications = current_user.notifications.sorted + end + + protected + def consume_notifications + current_user.notifications.unread.update(unread: false) + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 000000000..16854954d --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,17 @@ +class PagesController < ApplicationController + + skip_before_filter :require_login + + def home + if logged_in? + redirect_to :dashboard + end + end + + def fbjssdk_channel + response.headers["Pragma"] = "public" + response.headers["Cache-Control"] = "max-age=#{1.year.to_i}" + response.headers["Expires"] = 1.year.from_now.httpdate + render layout: false + end +end diff --git a/app/controllers/references_controller.rb b/app/controllers/references_controller.rb new file mode 100644 index 000000000..1f4fd04de --- /dev/null +++ b/app/controllers/references_controller.rb @@ -0,0 +1,47 @@ +class ReferencesController < ApplicationController + + before_filter :set_and_check_reference, except: :index + + def index + @references = current_user.references.visible + end + + def edit + @reference.outgoing_reference.content_required = true + end + + def update + @reference.outgoing_reference.content_required = true + if @reference.outgoing_reference.update_attributes(params[:incoming_reference][:outgoing_reference_attributes]) + redirect_to calendar_path, flash: { success: t('flash.success.reference_updated') } + else + flash.now[:error] = @reference.errors.full_messages + render "edit" + end + end + + def not_relevant + @reference.outgoing_reference.content_required = false + if @reference.outgoing_reference.update_attributes(relevant: false) + redirect_to calendar_path, flash: { success: t('flash.success.reference_updated') } + else + flash.now[:error] = @reference.errors.full_messages + render "edit" + end + end + + def relevant + @reference.outgoing_reference.content_required = false + if @reference.outgoing_reference.update_attributes(relevant: true) + flash.now[:success] = t('flash.success.reference_updated') + else + flash.now[:error] = @reference.errors.full_messages + end + render "edit" + end + + def set_and_check_reference + @reference = current_user.references.find(params[:id]) + redirect_to calendar_path, flash: { error: t('flash.error.reference_updated') } unless @reference.visible? + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 000000000..8dcba8086 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,17 @@ +class SessionsController < ApplicationController + + skip_before_filter :require_login, except: [:destroy] + + def create + user = User.from_omniauth(env["omniauth.auth"]) + session[:user_id] = user.id.to_s # keep the session simple + redirect_to root_url + end + + def destroy + if session[:user_id] + session[:user_id] = nil + end + redirect_to root_url + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 000000000..7cdd1e9c4 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,88 @@ +class UsersController < ApplicationController + + skip_before_filter :require_login, only: [:new, :create, :activate] + + before_filter :set_user_as_current_user, only: [:dashboard, :settings] + before_filter :check_admin, only: [:index, :ban, :unban] + + # TODO proper edit methods + + def index + @users = User.sorted.page params[:page] + end + + def show + @user = User.any_of({ username: params[:id] }, { uid: params[:id] }, { _id: params[:id] }).first + @facebook_details = @user.facebook_profile_batch(current_user != @user ? current_user : nil) + end + + def create + end + + def edit + end + + def update + @user = current_user + if @user.update_attributes(params[:user]) + redirect_to :settings, flash: { success: t('flash.user.success.update') } + else + render :settings + end + end + + def destroy + if current_user.admin? && params[:id].present? + @user = User.any_of({ username: params[:id] }, { uid: params[:id] }, { _id: params[:id] }).first + else + @user = current_user + end + if @user.destroy + session[@user.id] = nil + if current_user.admin? && @user != current_user + redirect_to users_path + else + redirect_to root_path, flash: { success: t('flash.user.success.destroy') } + end + else + redirect_to root_path, flash: { error: t('flash.user.error.destroy') } + end + end + + def banned + redirect_to root_path unless current_user.banned? + end + + def ban + @user = User.any_of({ username: params[:id] }, { uid: params[:id] }, { _id: params[:id] }).first + + # Prevent autoban + if @user == current_user + redirect_to users_path, flash: { error: t('flash.user.error.ban') } + return + end + + @user.banned = true + if @user.save + redirect_to users_path, flash: { success: t('flash.user.success.ban') } + else + redirect_to users_path, flash: { error: t('flash.user.error.ban') } + end + end + + def unban + @user = User.any_of({ username: params[:id] }, { uid: params[:id] }, { _id: params[:id] }).first + @user.banned = false + if @user.save + redirect_to users_path, flash: { success: t('flash.user.success.unban') } + else + redirect_to users_path, flash: { error: t('flash.user.error.unban') } + end + end + +private + + def set_user_as_current_user + @user = current_user + end +end diff --git a/app/form_builders/bootstrap_form_builder.rb b/app/form_builders/bootstrap_form_builder.rb new file mode 100644 index 000000000..c3bea7c3c --- /dev/null +++ b/app/form_builders/bootstrap_form_builder.rb @@ -0,0 +1,82 @@ +class BootstrapFormBuilder < ActionView::Helpers::FormBuilder + delegate :content_tag, :tag, :concat, to: :@template + + %w[text_field text_area password_field collection_select select date_select time_zone_select datetime_select].each do |method_name| + define_method(method_name) do |name, *args| + content_tag :div, class: "control-group#{" error" if object.errors.include?(name)}" do + label_field(name, *args) + + content_tag(:div, class: "controls") do + if name == :nationality + super(name, *args) + + content_tag(:span, class: "inline-flag") do + content_tag(:i, nil, id: "flag", class: ("flag-#{object.nationality.downcase}" if object.nationality?)) + end + else + super(name, *args) + end + + if object.errors.include?(name) + content_tag(:span, class: "help-inline") do + object.errors.messages[name].join(", ") + end + end + + help_field(name, *args) + end + end + end + end + + def default_tag(method_name, name, *args) + self.class.superclass.instance_method(method_name).bind(self).call(name, *args) + end + + def check_box(name, *args) + label(name, *args, class: "checkbox") do + super(name) + " " + object.class.human_attribute_name(name) + end + end + + def collection_check_boxes(attribute, records, record_id, record_name) + content_tag :div, class: "field" do + @template.hidden_field_tag("#{object_name}[#{attribute}][]") + + records.map do |record| + element_id = "#{object_name}_#{attribute}_#{record.send(record_id)}" + checkbox = @template.check_box_tag("#{object_name}[#{attribute}][]", + record.send(record_id), + object.send(attribute).include?(record.send(record_id)), + id: element_id) + checkbox + " " + @template.label_tag(element_id, record.send(record_name)) + end.join(tag(:br)) + end + end + + def error_messages + if object.errors.full_messages.any? + content_tag(:div, class: "alert alert-block alert-error fade in") do + concat content_tag(:button, "×", { class: "close", data: { dismiss: "alert" }, type: "button" }, false) + concat content_tag(:h4, I18n.t("helpers.form.error")) + object.errors.full_messages.map do |msg| + concat msg + concat tag(:br) + end + end + end + end + +private + + def label_field(name, *args) + options = args.extract_options! + required = object.class.validators_on(name).any? { |v| v.kind_of? ActiveModel::Validations::PresenceValidator } + label(name, options[:label], class: "control-label#{" required" if required}") + end + + def help_field(name, *args) + options = args.extract_options! + content_tag(:p, class: "help-block") { @template.t(".#{name}_help") } if options[:help] + end + + def objectify_options(options) + super.except(:help) + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 000000000..e6d1c313f --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,52 @@ +module ApplicationHelper + + def title(page_title) + content_for(:title) { page_title.to_s } + end + + def yield_or_default(section, default = "") + content_for?(section) ? content_for(section) + (" | #{APPNAME}" unless (logged_in? || content_for(section) == APPNAME)) : default + end + + def twitterized_type(type) + case type + when :alert + "warning" + when :error + "error" + when :notice + "info" + when :success + "success" + else + type.to_s + end + end + + def transparent_gif_image_data + "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + end + + def check_root + params[:controller].eql?('pages') and params[:action].eql?('home') + end + + def bootstrap_form_for(object, options = {}, &block) + options[:builder] = BootstrapFormBuilder + form_for(object, options, &block) + end + + def options_for_array_collection(model, attr_name, *args) + options_for_select("#{model}::#{attr_name.to_s.upcase}".safe_constantize.map { |e| [ model.human_attribute_name("#{attr_name}_#{e}"), e] }, *args) + end + + def collection_count(collection) + "(#{collection.size})" if collection.any? + end + + def share_on_timeline_available + Resque.workers.any? + rescue + false + end +end diff --git a/app/helpers/conversations_helper.rb b/app/helpers/conversations_helper.rb new file mode 100644 index 000000000..df99f2202 --- /dev/null +++ b/app/helpers/conversations_helper.rb @@ -0,0 +1,18 @@ +module ConversationsHelper + def message_classes(alternate, message, current_user) + [("alternate" if alternate), ("unread" if message.sender != current_user && message.unread?)].join(" ") + end + + def message_timestamp(message) + content_tag(:small, title: I18n.l(message.created_at.in_time_zone(current_user.time_zone), format: :long), class: "pull-right shy") do + I18n.t("conversations.messages.time_ago", time: distance_of_time_in_words(message.created_at, Time.now.utc, true)) + end + end + + def message_readat(message) + content_tag(:small, class: "shy") do + content_tag(:i, nil, class: "icon-ok") + " " + + I18n.t("conversations.messages.seen", date: l(message.read.in_time_zone(current_user.time_zone), format: :short)) + end + end +end diff --git a/app/helpers/itineraries_helper.rb b/app/helpers/itineraries_helper.rb new file mode 100644 index 000000000..3a687840f --- /dev/null +++ b/app/helpers/itineraries_helper.rb @@ -0,0 +1,5 @@ +module ItinerariesHelper + def boolean_options_for_select + @boolean_options_for_select ||= options_for_select([[t("boolean.true"),true], [t("boolean.false"),false]]) + end +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb new file mode 100644 index 000000000..253a77ee8 --- /dev/null +++ b/app/helpers/notifications_helper.rb @@ -0,0 +1,28 @@ +module NotificationsHelper + def notification_message(notification) + actor = User.find(notification.actor_id) + actor_link = content_tag(:a, actor, href: user_path(actor)) + case notification.class + when Notifications::HospitalityRequest + link = content_tag(:a, HospitalityRequest.model_name.human.downcase, href: show_or_edit_incoming_invite(notification.request, notification.incoming_invite)) + t(notification.translation_key, user: actor_link, hospitality_request: link).html_safe + when Notifications::HospitalityOutgoingInvite + link = content_tag(:a, HospitalityOutgoingInvite.model_name.human.downcase, href: show_or_edit_outgoing_invite(notification.outgoing_invite)) + t(notification.translation_key, user: actor_link, hospitality_outgoing_invite: link).html_safe + when Notifications::Reference + link = content_tag(:a, Reference.model_name.human.downcase, href: edit_incoming_reference_path(notification.reference_id)) + t(notification.translation_key, user: actor_link, reference: link).html_safe + end + end + + def notification_icon(notification) + case notification.class + when Notifications::HospitalityRequest + content_tag(:i, nil, class: "icon-plane") + when Notifications::HospitalityOutgoingInvite + content_tag(:i, nil, class: "icon-home") + when Notifications::Reference + content_tag(:i, nil, class: "icon-thumbs-up") + end + end +end diff --git a/app/helpers/references_helper.rb b/app/helpers/references_helper.rb new file mode 100644 index 000000000..ad74f452e --- /dev/null +++ b/app/helpers/references_helper.rb @@ -0,0 +1,86 @@ +module ReferencesHelper + + def make_stars(stars, text = true) + stars = stars.to_i + return unless stars + (stars.times.map do + content_tag(:i, nil, class: "minstrels-star icon-star", title: "*") + end + + (5 - stars).times.map do + content_tag(:i, nil, class: "minstrels-star icon-star-empty") + end).join.html_safe + + if text + content_tag(:small, " #{t("references.stars", count: stars)}").html_safe + end + end + + def make_thumbs(rating) + if rating == -1 + content_tag(:i, nil, class: "icon-thumbs-down") + elsif rating == 1 + content_tag(:i, nil, class: "icon-thumbs-up") + end + end + + def reference_help_text_for(reference) + if reference.hospitality? + if reference.is_host + t(".reference_hosting_help") + elsif reference.is_guest + t(".reference_travelling_help") + end + end + end + + def reference_snippet(reference) + [ make_thumbs(reference.rating), + " ", + content_tag(:b) do + link_to reference.referencing_user, user_path(reference.referencing_user) + end, + ": ", + content_tag(:em) do + "«#{reference.message}»".html_safe + end, + " ", + if reference.is_host + content_tag(:i, nil, class: "icon-home") + else + content_tag(:i, nil, class: "icon-plane") + end + ].join.html_safe + end + + def references_summary_tags(user) + content_tag(:p, class: "tags") do + content_tag(:span, "#{user.references.visible.filled_up.positive.size} positive") + + content_tag(:span, "#{user.references.visible.filled_up.neutral.size} neutral") + + content_tag(:span, "#{user.references.visible.filled_up.negative.size} negative") + end + end + + def reference_status_for(reference) + if reference.hospitality? + if reference.is_host + invites = current_user.hospitality_outgoing_invite_days.where(outgoing_invite_id: reference.experience_id) + cancelled_invites = invites.where(state: "cancelled") + accepted_invites = invites.where(state: "accepted") + declined_invites = invites.where(state: "declined") + "#{t(".outgoing_invite_status", count: invites.size, name: reference.referencing_user.name)} #{t(".host_outgoing_invites_cancelled", count: cancelled_invites.size)} + #{t(".host_incoming_invites_accepted", count: accepted_invites.size)} #{t(".host_incoming_invites_declined", count: declined_invites.size)}" + elsif reference.is_guest + invites = current_user.hospitality_request_days.where(request_id: reference.experience_id, + :"incoming_invite_days.inviting_user_id" => reference.referencing_user.id) + accepted_invites = invites.where(:"incoming_invite_days.inviting_user_id" => reference.referencing_user.id, + :"incoming_invite_days.state" => "accepted") + declined_invites = invites.where(:"incoming_invite_days.inviting_user_id" => reference.referencing_user.id, + :"incoming_invite_days.state" => "declined") + cancelled_invites = invites.where(:"incoming_invite_days.inviting_user_id" => reference.referencing_user.id, + :"incoming_invite_days.state" => "cancelled") + + "#{t(".incoming_invite_status", count: invites.size, name: reference.referencing_user.name)} #{t(".guest_incoming_invites_accepted", count: accepted_invites.size)} + #{t(".guest_incoming_invites_declined", count: declined_invites.size)} #{t(".guest_outgoing_invites_cancelled", count: cancelled_invites.size)}" + end + end + end +end \ No newline at end of file diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 000000000..68ad7b221 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,26 @@ +module UsersHelper + + def facebook_profile_picture(user, type = :square) + "http://graph.facebook.com/#{user.class == User ? user.uid : user}/picture?type=#{type}" + end + + def user_profile_picture(user, opts = {}) + options = { size: [50, 50], + type: :square, + thumbnail: true, + html: {} + }.merge(opts) + tag(:img, + { width: ("#{options[:size][0]}px" if options[:size][0]), + height: ("#{options[:size][1]}px" if options[:size][1]), + src: facebook_profile_picture(user, options[:type]), + alt: "", + class: [("thumbnail" if options[:thumbnail]), options[:class]].compact.join(" ") }.merge(options[:html])) + end + + def nationality_flag(user, tooltip = true, tooltip_placement = "bottom") + options = { class: "flag-#{user.nationality.downcase}" } + options = options.merge({ rel: "tooltip", title: user.nationality_name, data: { placement: tooltip_placement } }) if tooltip + content_tag(:i, nil, options) if user.nationality? + end +end diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 000000000..828a54c7d --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,22 @@ +class UserMailer < ActionMailer::Base + + default from: APP_CONFIG.mailer.from + + def activation_needed_email(user) + @user = user + @url = "#{APP_CONFIG.base_url}/users/#{user.activation_token}/activate" + mail(to: user.email, subject: t('user_mailer.activation_needed_email.subject', website: "minstrels.com")) + end + + def activation_success_email(user) + @user = user + @url = "#{APP_CONFIG.base_url}/login" + mail(to: user.email, subject: t('user_mailer.activation_success_email.subject')) + end + + def reset_password_email(user) + @user = user + @url = "#{APP_CONFIG.base_url}/password_resets/#{user.reset_password_token}/edit" + mail(to: user.email, subject: t('user_mailer.reset_password_email.subject')) + end +end diff --git a/app/models/.gitkeep b/app/models/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/beta_invite.rb b/app/models/beta_invite.rb new file mode 100644 index 000000000..130b66263 --- /dev/null +++ b/app/models/beta_invite.rb @@ -0,0 +1,10 @@ +class BetaInvite + include Mongoid::Document + + attr_accessible :email + + field :email + + validates :email, uniqueness: true, format: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/ + +end \ No newline at end of file diff --git a/app/models/conversation.rb b/app/models/conversation.rb new file mode 100644 index 000000000..b84a7b497 --- /dev/null +++ b/app/models/conversation.rb @@ -0,0 +1,17 @@ +class Conversation + include Mongoid::Document + include Mongoid::Timestamps + include Mongoid::Paranoia + + #attr_accessible :messages_attributes + + embeds_many :messages, cascade_callbacks: true + #accepts_nested_attributes_for :messages, reject_if: ->(attributes) { attributes['body'].blank? } + + has_and_belongs_to_many :users + belongs_to :conversable, polymorphic: true + + def mark_as_read(user) + messages.unread.where(:sender_id.ne => user.id).update_all(read: Time.now.utc) + end +end diff --git a/app/models/country.rb b/app/models/country.rb new file mode 100644 index 000000000..3fd2f6253 --- /dev/null +++ b/app/models/country.rb @@ -0,0 +1,16 @@ +class Country + include Mongoid::Document + + attr_accessible :name, :code, :name_translations + + field :name, localize: true + field :code + + scope :sorted, asc(:"name.en-US") #NOTE needed for development + + index :code + + def _name + self.name_translations["en-US"] #NOTE needed for development + end +end diff --git a/app/models/feedback.rb b/app/models/feedback.rb new file mode 100644 index 000000000..ce9cb8265 --- /dev/null +++ b/app/models/feedback.rb @@ -0,0 +1,31 @@ +class Feedback + include Mongoid::Document + include Mongoid::Timestamps + + TYPE = ["bug", "idea"] + STATUS = ["open", "fixed", "in progress", "not applicable"] + + attr_accessible :type, :status, :message, :url + + belongs_to :user + delegate :name, to: :user, prefix: true + + field :type, type: String + field :message, type: String + field :url, type: String + field :status, type: String, default: "open" + + validates :type, inclusion: TYPE, presence: true + validates :status, inclusion: STATUS, presence: true + validates :message, presence: true + + def short_message(max = 100) + return message if !message? || message.size < (max-3) + last_space = message[0...(max-3)].rindex(" ") || (max-3) + message[0...last_space] << "..." + end + + def fixed? + status == "fixed" + end +end diff --git a/app/models/itinerary.rb b/app/models/itinerary.rb new file mode 100644 index 000000000..d61d1b8a5 --- /dev/null +++ b/app/models/itinerary.rb @@ -0,0 +1,177 @@ +class Itinerary + include Mongoid::Document + include Mongoid::Paranoia + include Mongoid::Geospatial + include Mongoid::MultiParameterAttributes + + VEHICLE = %w(car motorcycle van) + DAYNAME = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday) + + attr_accessible :title, :description, :vehicle, :num_people, :smoking_allowed, :pets_allowed, :fuel_cost, :tolls + attr_accessible :round_trip, :leave_date, :return_date + attr_accessible :share_on_timeline + + belongs_to :user + delegate :name, to: :user, prefix: true + delegate :first_name, to: :user, prefix: true + + has_many :conversations, as: :conversable + + # Route + field :start_location, type: Point, spatial: true + field :end_location, type: Point, spatial: true + field :via_waypoints, type: Array + field :overview_path, type: Array + field :overview_polyline, type: String + + # Details + field :title + field :description + field :vehicle, default: "car" + field :num_people, type: Integer + field :smoking_allowed, type: Boolean, default: false + field :pets_allowed, type: Boolean, default: false + field :fuel_cost, type: Integer + field :tolls, type: Integer + field :round_trip, type: Boolean, default: false + field :leave_date, type: DateTime, default: -> { (Time.now).change(min: (Time.now.min / 10) * 10) + 10.minutes } + field :return_date, type: DateTime, default: -> { (Time.now).change(min: (Time.now.min / 10) * 10) + 70.minutes } + field :recurrent, type: Boolean, default: false + + # Cached user details (for filtering purposes) + field :driver_gender + + attr_accessor :route_json_object, :share_on_timeline + + spatial_index :start_location + spatial_index :end_location + + #default_scope -> { any_of({:leave_date.gte => Time.now.utc}, {:return_date.gte => Time.now.utc, round_trip: true}, { recurrent: true }) } + + validates :title, length: { maximum: 40 }, presence: true + validates :description, length: { maximum: 1000 }, presence: true + validates :vehicle, inclusion: VEHICLE + validates :num_people, numericality: { only_integer: true, greater_than: 0, less_than: 10 }, allow_blank: true + validates :fuel_cost, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 10000 } + validates :tolls, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 10000 } + + validates :leave_date, timeliness: { on_or_after: -> { Time.now } }, on: :create + validate :return_date_validator, if: -> { round_trip } + + def return_date_validator + self.errors.add(:return_date, I18n.t("mongoid.errors.messages.after", restriction: leave_date.strftime(I18n.t("validates_timeliness.error_value_formats.datetime")))) if return_date <= leave_date + end + + def self.build_with_route_json_object(params, user) + new(params) do |itinerary| + route_json_object = JSON.parse(params[:route_json_object]) + itinerary.start_location = { lat: route_json_object["start_location"]["lat"], + lng: route_json_object["start_location"]["lng"] } + itinerary.end_location = { lat: route_json_object["end_location"]["lat"], + lng: route_json_object["end_location"]["lng"] } + itinerary.via_waypoints = route_json_object["via_waypoints"] + itinerary.overview_path = route_json_object["overview_path"] + itinerary.overview_polyline = route_json_object["overview_polyline"] + + itinerary.user = user + + itinerary.driver_gender = user.gender + end + end + + def self.search(params) + # TODO: Optimization + itineraries = Itinerary.where(get_boolean_filters(params)) + + itineraries_start = itineraries.includes(:user).where(:start_location.near(:sphere) => { point: [params[:start_location_lat], params[:start_location_lng]], + max: 5, + unit: :km }) + itineraries_end = Itinerary.where(:end_location.near(:sphere) => { point: [params[:end_location_lat], params[:end_location_lng]], + max: 5, + unit: :km }) + + itineraries_start & itineraries_end + rescue + end + + def to_latlng_array(field) + return unless [:start_location, :end_location].include?(field) + [ self[field]["lat"], self[field]["lng"] ] + end + + def sample_path(precision = 10) + overview_path.in_groups(precision).map{ |g| g.first }.insert(-1,overview_path.last) + end + + def static_map + URI.encode("http://maps.googleapis.com/maps/api/staticmap?size=200x200&sensor=false&markers=color:green|label:B|#{to_latlng_array(:end_location).join(",")}&markers=color:green|label:A|#{to_latlng_array(:start_location).join(",")}&path=enc:#{overview_polyline}") + end + + def start_location + # GOD(DAMN) mongoid_geospatial + @start_location ||= self[:start_location] + end + + def end_location + # GOD(DAMN) mongoid_geospatial + @end_location ||= self[:end_location] + end + + def random_close_location(max_dist = 0.5, km = true) + # Thanks to http://www.geomidpoint.com/random/calculation.html + + return unless source && source[:lat] != nil && source[:lng] != nil + + deg_to_rad = Math::PI / 180 + rad_to_deg = 180 / Math::PI + radius_earth_M = 3960.056052 + radius_earth_Km = 6372.796924 + + rand1 = rand + rand2 = rand + + # Convert all latitudes and longitudes to radians + start_lat = source[:lat] * deg_to_rad + start_lng = source[:lng] * deg_to_rad + + # Convert maximum distance to radians. + max_dist_rad = max_dist / (km ? radius_earth_Km : radius_earth_M) + + # Compute a random distance from 0 to maxdist scaled + # so that points on larger circles have a greater probability + # of being chosen than points on smaller circles as described earlier. + dist = Math::acos( rand1 * (Math::cos(max_dist_rad) - 1) + 1 ) + + # Compute a random bearing from 0 to 2*PI radians (0 to 360 degrees), + # with all bearings having an equal probability of being chosen. + brg = 2 * Math::PI * rand2 + + # Use the starting point, random distance and random bearing to calculate the coordinates of the final random point. + lat = Math::asin( Math::sin(start_lat) * Math::cos(dist) + Math::cos(start_lat) * Math::sin(dist) * Math::cos(brg) ) + lng = start_lng + Math::atan2( Math::sin(brg) * Math::sin(dist) * Math::cos(start_lat), Math::cos(dist) - Math::sin(start_lat) * Math.sin(lat) ) + + if (lng < -1 * Math::PI) + lng = lng + 2 * Math::PI + elsif (lng > Math::PI) + lng = lng - 2 * Math::PI + end + + [lat * rad_to_deg, lng * rad_to_deg] + end + + def to_s + title || super() + end + +private + def self.get_boolean_filters(params = {}) + filters = {} + [:smoking_allowed, :pets_allowed, :round_trip].each do |boolean_field| + param = params["filter_#{boolean_field}".to_sym] + filters.merge!(boolean_field => (param == "true")) unless param.blank? + end + filter_driver_gender_param = params[:filter_driver_gender] + filters.merge!(driver_gender: filter_driver_gender_param) if User::GENDER.include?(filter_driver_gender_param) + filters + end +end diff --git a/app/models/itinerary_observer.rb b/app/models/itinerary_observer.rb new file mode 100644 index 000000000..7e7fbce56 --- /dev/null +++ b/app/models/itinerary_observer.rb @@ -0,0 +1,8 @@ +class ItineraryObserver < Mongoid::Observer + def after_create(itinerary) + if itinerary.share_on_timeline + Resque.enqueue(FacebookTimelineUpdater, itinerary.id) + end + rescue + end +end diff --git a/app/models/itinerary_route.rb b/app/models/itinerary_route.rb new file mode 100644 index 000000000..9fe461762 --- /dev/null +++ b/app/models/itinerary_route.rb @@ -0,0 +1,20 @@ +class ItineraryRoute + include ActiveModel::Validations + include ActiveModel::Conversion + extend ActiveModel::Naming + + attr_accessor :from, :to, :avoid_highways, :avoid_tolls + + validates :from, presence: true + validates :to, presence: true + + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end + end + + def persisted? + false + end +end diff --git a/app/models/itinerary_search.rb b/app/models/itinerary_search.rb new file mode 100644 index 000000000..6e0328dd8 --- /dev/null +++ b/app/models/itinerary_search.rb @@ -0,0 +1,23 @@ +class ItinerarySearch + include ActiveModel::Validations + include ActiveModel::Conversion + extend ActiveModel::Naming + + attr_accessor :from, :to + attr_accessor :start_location_lat, :start_location_lng, :end_location_lat, :end_location_lng + attr_accessor :filter_smoking_allowed, :filter_pets_allowed, :filter_round_trip + attr_accessor :filter_driver_gender + + validates :from, presence: true + validates :to, presence: true + + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end + end + + def persisted? + false + end +end diff --git a/app/models/message.rb b/app/models/message.rb new file mode 100644 index 000000000..edcbcfa06 --- /dev/null +++ b/app/models/message.rb @@ -0,0 +1,23 @@ +class Message + include Mongoid::Document + include Mongoid::Timestamps + include Mongoid::Paranoia + + attr_accessible :body, :sender, :read + + belongs_to :sender, class_name: User.model_name + + embedded_in :conversation + + field :body + field :read, type: DateTime, default: nil + + validates :body, presence: true, length: { maximum: 1000 } + validates :sender, presence: true + + scope :unread, where(read: nil) + + def unread? + read.nil? + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 000000000..c248bc6f2 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,12 @@ +class Notification + include Mongoid::Document + include Mongoid::Timestamps + include Mongoid::Paranoia + + embedded_in :user + + field :read, type: Boolean, default: false + + scope :unread, where(read: false) + scope :sorted, desc(:created_at) +end diff --git a/app/models/notifications/hospitality_outgoing_invite.rb b/app/models/notifications/hospitality_outgoing_invite.rb new file mode 100644 index 000000000..e1fc7ebf0 --- /dev/null +++ b/app/models/notifications/hospitality_outgoing_invite.rb @@ -0,0 +1,18 @@ +class Notifications::HospitalityOutgoingInvite < Notification + field :actor_id, type: BSON::ObjectId + field :outgoing_invite_id, type: BSON::ObjectId + field :inquiry, type: Boolean + field :message_only, type: Boolean + + def translation_key + if message_only? + 'notifications.hospitality_outgoing_invite_message_received' + else + 'notifications.hospitality_outgoing_invite_updated' + end + end + + def outgoing_invite + @outgoing_invite ||= user.hospitality_outgoing_invites.find(outgoing_invite_id) + end +end diff --git a/app/models/notifications/hospitality_request.rb b/app/models/notifications/hospitality_request.rb new file mode 100644 index 000000000..aa2a8a2a6 --- /dev/null +++ b/app/models/notifications/hospitality_request.rb @@ -0,0 +1,34 @@ +class Notifications::HospitalityRequest < Notification + field :actor_id, type: BSON::ObjectId + field :request_id, type: BSON::ObjectId + field :incoming_invite_id, type: BSON::ObjectId + field :inquiry, type: Boolean + field :message_only, type: Boolean + field :is_new, type: Boolean + + def translation_key + if message_only? + 'notifications.hospitality_request_message_received' + elsif inquiry? + if is_new? + 'notifications.hospitality_request_inquiry_received' + else + 'notifications.hospitality_request_inquiry_updated' + end + else + if is_new? + 'notifications.hospitality_request_invite_received' + else + 'notifications.hospitality_request_invite_updated' + end + end + end + + def request + @request ||= user.hospitality_requests.find(request_id) + end + + def incoming_invite + @incoming_invite ||= @request.incoming_invites.find(incoming_invite_id) + end +end diff --git a/app/models/notifications/reference.rb b/app/models/notifications/reference.rb new file mode 100644 index 000000000..caed60af5 --- /dev/null +++ b/app/models/notifications/reference.rb @@ -0,0 +1,18 @@ +class Notifications::Reference < Notification + field :actor_id, type: BSON::ObjectId + field :reference_id, type: BSON::ObjectId + field :relevant, type: Boolean + field :is_new, type: Boolean + + def translation_key + if !relevant? + 'notifications.reference_not_relevant' + else + if is_new? + 'notifications.reference_created' + else + 'notifications.reference_updated' + end + end + end +end diff --git a/app/models/reference.rb b/app/models/reference.rb new file mode 100644 index 000000000..038b6c5bb --- /dev/null +++ b/app/models/reference.rb @@ -0,0 +1,15 @@ +class Reference + include Mongoid::Document + include Mongoid::Timestamps + include Mongoid::Paranoia + + attr_accessible :message, :rating + + belongs_to :referencing_user, class_name: User.model_name + + field :message + field :rating, type: Integer + + validates :message, presence: true + validates :rating, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: -1, less_than_or_equal_to: 1 } +end diff --git a/app/models/references/incoming_reference.rb b/app/models/references/incoming_reference.rb new file mode 100644 index 000000000..f1f6832c8 --- /dev/null +++ b/app/models/references/incoming_reference.rb @@ -0,0 +1,49 @@ +class Reference::Incoming < Reference + + embedded_in :user + + embeds_one :outgoing_reference + accepts_nested_attributes_for :outgoing_reference + + attr_accessible :experience_class, :experience_id, :related_reference_id, :visible_from + + field :experience_class + field :experience_id, type: BSON::ObjectId + field :visible_from, type: Date + field :related_reference_id, type: BSON::ObjectId + + validates :related_reference_id, presence: true, on: :create + + scope :visible, -> { where(:visible_from.lte => Date.today.midnight.utc) } + + scope :filled_up, where(:message.ne => nil, :rating.ne => nil, relevant: true) + + scope :hospitalities, where(experience_class: /^Hospitality/) + + scope :pending, -> { where( :"outgoing_reference.message" => nil, :"outgoing_reference.rating" => nil, :"outgoing_reference.relevant" => true) } + scope :filled_up_by_me, -> { where( :"outgoing_reference.message".ne => nil, :"outgoing_reference.rating".ne => nil, :"outgoing_reference.relevant" => true) } + + scope :positive, where(rating: 1) + scope :neutral, where(rating: 0) + scope :negative, where(rating: -1) + + def related_reference + @related_reference ||= referencing_user.references.find(related_reference_id) + end + + def is_guest + @is_guest ||= (experience_class == HospitalityRequest.model_name) + end + + def is_host + @is_host ||= (experience_class == HospitalityOutgoingInvite.model_name) + end + + def hospitality? + (experience_class == HospitalityRequest.model_name) || (experience_class == HospitalityOutgoingInvite.model_name) + end + + def visible? + Date.today.midnight.utc >= visible_from.at_midnight.utc + end +end diff --git a/app/models/references/outgoing_reference.rb b/app/models/references/outgoing_reference.rb new file mode 100644 index 000000000..455d8bb7b --- /dev/null +++ b/app/models/references/outgoing_reference.rb @@ -0,0 +1,34 @@ +class Reference::Outgoing < Reference + + embedded_in :incoming_reference + + validates :message, presence: true, on: :update, if: ->{ content_required } + validates :rating, numericality: { only_integer: true, greater_than_or_equal_to: -1, less_than_or_equal_to: 1 }, if: ->{ content_required } + validate :doable?, on: :update + + attr_accessor :content_required + + set_callback(:update, :after) do + @is_new = !incoming_reference.related_reference.message? + incoming_reference.related_reference.update_attributes(message: message, + rating: rating, + relevant: relevant) + end + + set_callback(:save, :after) do + # Notifications + if changed? + notification = Notifications::Reference.new(actor_id: incoming_reference.user.id, + reference_id: incoming_reference.related_reference_id, + relevant: relevant, + is_new: @is_new) + incoming_reference.referencing_user.notifications << notification + end + end + + def doable? + if changed? && !incoming_reference.visible? + self.errors.add(:base, "You cannot leave a reference now") + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..0996f5f83 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,217 @@ +class User + include Mongoid::Document + include Mongoid::Timestamps + include Mongoid::Paranoia + + GENDER = %w(male female) + + paginates_per 25 + + attr_accessible :nationality, :time_zone, :locale, :vehicle_avg_consumption + + has_and_belongs_to_many :conversations + + has_many :itineraries, dependent: :destroy + has_many :feedbacks + + embeds_many :notifications + embeds_many :references, cascade_callbacks: true + + # Auth / Credentials + field :provider + field :uid + field :oauth_token + field :oauth_expires_at + # Cache Permissions + field :facebook_permissions + # Info + field :email + field :name + # Extra + field :username + field :gender + field :bio + field :languages, type: Hash + + # More info requiring special permissions + field :birthday, type: Date + field :work, type: Hash, default: {} + field :education, type: Hash, default: {} + + # Icare + field :nationality + field :vehicle_avg_consumption, type: Float, default: (1.741*7.0/100.0).round(2) + + # Account + field :locale + field :time_zone, default: "UTC" # NOTE think about it + field :telephone + field :admin, type: Boolean, default: false + #field :access_level, type: Integer, default: 0 + field :banned, type: Boolean, default: false + + field :send_email_messages, type: Boolean, default: true + field :send_email_references, type: Boolean, default: true + + validates :gender, inclusion: GENDER, allow_blank: true + validates :nationality, inclusion: Country.all.map{ |c| c.code }, allow_blank: true + validates :time_zone, inclusion: ActiveSupport::TimeZone.zones_map.map{ |zone| zone.first }, allow_blank: true + validates :vehicle_avg_consumption, numericality: { greater_than: 0, less_than: 10 }, presence: true + #validates :access_level, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 5 } + scope :sorted, asc(:name) + + def self.from_omniauth(auth) + if user = where(auth.slice("provider", "uid")).first + user.update_fields_from_omniauth(auth) + user.save! + user + else + create_from_omniauth(auth) + end + end + + def self.create_from_omniauth(auth) + create! do |user| + + # Auth / Credentials + user.provider = auth.provider + user.uid = auth.uid + user.oauth_token = auth.credentials.token + user.oauth_expires_at = Time.at(auth.credentials.expires_at) + + user.update_fields_from_omniauth(auth) + end + end + + def update_fields_from_omniauth(auth) + # Info + self.email = auth.info.email + self.name = auth.info.name + + # Extra + self.username = auth.extra.raw_info.username + self.gender = auth.extra.raw_info.gender + self.bio = auth.extra.raw_info.bio + self.languages = auth.extra.raw_info.languages + + # Locale (with priority to icare) + self.locale = auth.extra.raw_info.locale.gsub(/_/,"-") unless self.locale? + + # Extras (extra permissions required) + self.birthday = Date.strptime(auth.extra.raw_info.birthday, "%m/%d/%Y").at_midnight + self.work = auth.extra.raw_info.work + self.education = auth.extra.raw_info.education + + # Cache permissions + facebook do |fb| + self.facebook_permissions = fb.get_connections("me", "permissions")[0] + end + end + + def facebook + @facebook ||= Koala::Facebook::API.new(oauth_token) + block_given? ? yield(@facebook) : @facebook + rescue Koala::Facebook::APIError => e + logger.info e.to_s + nil # or consider a custom null object + # TODO + # Koala::Facebook::APIError: OAuthException: Error validating access token: Session does not match current stored session. This may be because the user changed the password since the time the session was created or Facebook has changed the session for security reasons. + end + + def has_facebook_permission(scope) + facebook_permissions? ? facebook_permissions[scope.to_s].to_i == 1 : false + end + + def facebook_connections(connection) + facebook { |fb| fb.get_connections("me", connection.to_s) } + end + + def facebook_likes + # TODO cache!!!!! + #@facebook_likes ||= facebook_connections(:likes) + fb_favorites = ["music", "books", "movies", "television", "games", "activities", "interests"] #"athletes", "sports_teams", "sports", "inspirational_people" + facebook.batch do |batch_api| + fb_favorites.each do |favorite| + batch_api.get_connections('me', favorite) + end + end.flatten + end + + def friends_with_privacy(friends = 0) + case friends + when 0...10 + "10-" + when 10...100 + "#{friends/10}0+" + when 100...1000 + "#{friends/100}00+" + when 1000...5000 + "#{friends/1000}000+" + else + "5000" + end + end + + def facebook_profile_batch(other_user = nil) + fb_favorites = ["music", "books", "movies", "television", "games", "activities", "interests"] #"athletes", "sports_teams", "sports", "inspirational_people" + batch = facebook.batch do |batch_api| + batch_api.get_connections('me', "friends", limit: 1001) + batch_api.get_connections('me', "mutualfriends/#{other_user.uid}") if other_user + fb_favorites.each do |favorite| + batch_api.get_connections('me', favorite) + end + end + if other_user + { friends: friends_with_privacy(batch[0].size), mutualfriends: batch[1], likes: batch[2..-1].flatten } + else + { friends: friends_with_privacy(batch[0].size), likes: batch[1..-1].flatten } + end + end + + def email_inclusion + return if Rails.env.development? || Rails.env.test? || email =~ /@cs\.fake$/ + unless BetaInvite.where(email: email).first + self.errors.add(:email, "Sorry, minstrels is in private beta. You are not authorized to register.") + end + end + + def age + ((Time.now.at_midnight - birthday.at_midnight) / 1.year).floor if birthday? + end + + def age_sex_nationality + [age, (User.human_attribute_name("gender_#{gender}").downcase if gender?), nationality_name].compact.join(" / ") + end + + def nationality_name + Country.where(code: nationality).first._name if nationality? + end + + def short_about + shorten(about, 500, true) + end + + def first_name + @first_name ||= name.split[0] if name? + end + + def to_s + name || "" + end + + def to_param + username || uid || _id + end + + def profile_picture + "http://graph.facebook.com/#{uid}/picture?type=square" + end + + protected + + def shorten(text, max = 500, ellipsis = true) + return text if !text || text.size <= max + last_space = text[0...max].rindex(" ") || max + [text[0...last_space],("..." if ellipsis)].join() + end +end diff --git a/app/views/beta_invites/_form.html.haml b/app/views/beta_invites/_form.html.haml new file mode 100644 index 000000000..921c3b5f6 --- /dev/null +++ b/app/views/beta_invites/_form.html.haml @@ -0,0 +1,4 @@ += bootstrap_form_for @beta_invite, html: { class: 'form-horizontal' } do |f| + = f.text_field :email, class: 'text_field' + .form-actions + = f.submit class: 'btn btn-primary' diff --git a/app/views/beta_invites/index.html.haml b/app/views/beta_invites/index.html.haml new file mode 100644 index 000000000..417c0b047 --- /dev/null +++ b/app/views/beta_invites/index.html.haml @@ -0,0 +1,20 @@ +- model_class = BetaInvite.new.class +.page-header + %h1=t '.title', default: model_class.model_name.human.pluralize +%table.table.table-striped + %thead + %tr + %th= model_class.human_attribute_name(:email) + %th=t '.actions', default: t("helpers.actions") + %tbody + - @beta_invites.each do |beta_invite| + %tr + %td + %p + = beta_invite.email + - if User.where(email: beta_invite.email).first + %i.icon-ok + %td + = link_to t('.destroy', default: t("helpers.links.destroy")), beta_invite_path(beta_invite), method: :delete, data: { confirm: t("helpers.links.confirm") }, class: 'btn btn-mini btn-danger' + += link_to t('.new', default: t("helpers.links.new")), new_beta_invite_path, class: 'btn btn-primary' diff --git a/app/views/beta_invites/new.html.haml b/app/views/beta_invites/new.html.haml new file mode 100644 index 000000000..e958db71b --- /dev/null +++ b/app/views/beta_invites/new.html.haml @@ -0,0 +1,4 @@ +- model_class = @beta_invite.class +.page-header + %h1=t '.title', default: t('helpers.titles.new', model: model_class.model_name.human, default: "New #{model_class.model_name.human}") += render "form" diff --git a/app/views/conversations/_conversation.html.haml b/app/views/conversations/_conversation.html.haml new file mode 100644 index 000000000..102a59148 --- /dev/null +++ b/app/views/conversations/_conversation.html.haml @@ -0,0 +1,7 @@ +%ul.unstyled.conversation + - alternate = false + - prev_sender = nil + - @conversation.messages.each do |message| + - alternate = !alternate if prev_sender != message.sender && prev_sender + = render "message", message: message, alternate: alternate + - prev_sender = message.sender diff --git a/app/views/conversations/_form.html.haml b/app/views/conversations/_form.html.haml new file mode 100644 index 000000000..dcffc1037 --- /dev/null +++ b/app/views/conversations/_form.html.haml @@ -0,0 +1,7 @@ += bootstrap_form_for @conversation, validate: false, html: { class: "form-horizontal" } do |f| + = hidden_field_tag :itinerary_id, @itinerary.id if @itinerary + = f.error_messages + = f.fields_for @conversation.messages.build do |m| + = m.text_area :body, rows: 5, class: "input-xxlarge" + .form-actions + = f.submit class: "btn btn-primary" diff --git a/app/views/conversations/_message.html.haml b/app/views/conversations/_message.html.haml new file mode 100644 index 000000000..22ab72ed9 --- /dev/null +++ b/app/views/conversations/_message.html.haml @@ -0,0 +1,13 @@ +%li{ class: message_classes(alternate, message, current_user) } + .message-avatar + = user_profile_picture message.sender, size: [50, 50], thumbnail: false + .message + .message-header + = message_timestamp message + %span + %strong= link_to message.sender, user_path(message.sender) + %p.message-body + #{message.body} + - if (message.sender == current_user && !message.unread?) + %br + = message_readat message diff --git a/app/views/conversations/index.html.haml b/app/views/conversations/index.html.haml new file mode 100644 index 000000000..798de638f --- /dev/null +++ b/app/views/conversations/index.html.haml @@ -0,0 +1,16 @@ +- title t(".title") +.row + .span12 + %h1= yield :title + .span12 + %table.table.table-striped + %thead + %th Index + %th ID + %th{ colspan: "3" } Actions + %tbody + - @conversations.each_with_index do |conversation, index| + %tr + %td= index.next + %td= conversation.id + %td= link_to t("helpers.links.show"), conversation_path(conversation) diff --git a/app/views/conversations/new.html.haml b/app/views/conversations/new.html.haml new file mode 100644 index 000000000..67d3c814a --- /dev/null +++ b/app/views/conversations/new.html.haml @@ -0,0 +1,5 @@ +- title t(".title") +.row + .span12 + %h1.conversation-title= t(".start_conversation_about", itinerary_link: link_to(@itinerary, itinerary_path(@itinerary)), user_link: link_to(@itinerary.user, user_path(@itinerary.user))).html_safe + = render "form" \ No newline at end of file diff --git a/app/views/conversations/show.html.haml b/app/views/conversations/show.html.haml new file mode 100644 index 000000000..e014909f4 --- /dev/null +++ b/app/views/conversations/show.html.haml @@ -0,0 +1,7 @@ +- title t(".title") +.row + .span12 + %h1.conversation-title= t(".conversation_about", itinerary_link: link_to(@itinerary, itinerary_path(@itinerary)), user_link: link_to(@itinerary.user, user_path(@itinerary.user))).html_safe + = render "conversation" + %hr + = render "form" diff --git a/app/views/feedbacks/_form.html.haml b/app/views/feedbacks/_form.html.haml new file mode 100644 index 000000000..3dce38061 --- /dev/null +++ b/app/views/feedbacks/_form.html.haml @@ -0,0 +1,9 @@ += bootstrap_form_for @feedback, validate: true, html: { class: 'form-horizontal' } do |f| + = f.error_messages + = f.select :type, Feedback::TYPE + - if current_user.admin? + = f.select :status, Feedback::STATUS + = f.text_area :message, class: 'full-width border-box', rows: 8 + = f.text_field :url, class: 'full-width width-max' + .form-actions + = f.submit class: 'btn btn-primary' \ No newline at end of file diff --git a/app/views/feedbacks/edit.html.haml b/app/views/feedbacks/edit.html.haml new file mode 100644 index 000000000..3eb1e1d48 --- /dev/null +++ b/app/views/feedbacks/edit.html.haml @@ -0,0 +1,5 @@ +- model_class = @feedback.class +.row + .span12 + %h1=t '.title', default: t('helpers.titles.edit', model: model_class.model_name.human, default: "Edit #{model_class.model_name.human}") + = render "form" diff --git a/app/views/feedbacks/index.html.haml b/app/views/feedbacks/index.html.haml new file mode 100644 index 000000000..c59f05d87 --- /dev/null +++ b/app/views/feedbacks/index.html.haml @@ -0,0 +1,30 @@ +.row + .span12 + = render "shared/flash_messages" + %h1=t '.title', default: Feedback.model_name.human(count: :lot) + %table.table.table-striped + %thead + %tr + %th= Feedback.human_attribute_name(:type) + %th= Feedback.human_attribute_name(:status) + %th= User.model_name.human + %th= Feedback.human_attribute_name(:message) + %th= Feedback.human_attribute_name(:updated_at) + %th=t '.actions', default: t("helpers.actions") + %tbody + - @feedbacks.each do |feedback| + %tr{ class: ("success" if feedback.fixed?) } + %td= feedback.type + %td= feedback.status.upcase + %td= link_to feedback.user_name, feedback_path(feedback) + %td< + = feedback.short_message + %td= l feedback.updated_at, format: :long + %td + = link_to edit_feedback_path(feedback), class: 'btn btn-mini' do + = t("helpers.links.edit") + = link_to t("helpers.links.new"), new_feedback_path(url: @url), class: 'btn btn-primary' + - if params[:hide_fixed] + = link_to t('.show_fixed'), feedbacks_path, class: 'btn' + - else + = link_to t('.hide_fixed'), feedbacks_path(hide_fixed: true), class: 'btn' diff --git a/app/views/feedbacks/new.html.haml b/app/views/feedbacks/new.html.haml new file mode 100644 index 000000000..1d393f3da --- /dev/null +++ b/app/views/feedbacks/new.html.haml @@ -0,0 +1,5 @@ +- model_class = @feedback.class +.row + .span12 + %h1=t '.title', default: t('helpers.titles.new', model: model_class.model_name.human, default: "New #{model_class.model_name.human}") + = render "form" diff --git a/app/views/feedbacks/show.html.haml b/app/views/feedbacks/show.html.haml new file mode 100644 index 000000000..b7d8e03c9 --- /dev/null +++ b/app/views/feedbacks/show.html.haml @@ -0,0 +1,29 @@ +- model_class = @feedback.class +.row + .span12 + %h1=t '.title', default: model_class.model_name.human + + %p + %strong= model_class.human_attribute_name(:user_id) + ':' + %br + - if @feedback.user + = @feedback.user_name + - else + %i + \#Former User #{@feedback.user_id} + %p + %strong= model_class.human_attribute_name(:type) + ':' + %br + = @feedback.type + %p.pre-line>< + %strong><= model_class.human_attribute_name(:message) + ':' + %br + ~ @feedback.message.strip.gsub(/\r/,"") + %p + %strong= model_class.human_attribute_name(:url) + ':' + %br + = link_to @feedback.url, @feedback.url + + .form-actions + = link_to t('.back', default: t("helpers.links.back")), feedbacks_path, class: 'btn' + = link_to t('.edit', default: t("helpers.links.edit")), edit_feedback_path(@feedback), class: 'btn' diff --git a/app/views/itineraries/_confirm_and_share_step.html.haml b/app/views/itineraries/_confirm_and_share_step.html.haml new file mode 100644 index 000000000..39797966a --- /dev/null +++ b/app/views/itineraries/_confirm_and_share_step.html.haml @@ -0,0 +1,61 @@ +=# true_text NOT converted to true-text +#itinerary-preview{ data: { true_text: t("boolean.true"), false_text: t("boolean.false") }} + .row + .span3 + =# static_map_url_builder converted to static-map-url-builder + = image_tag transparent_gif_image_data, class: "thumbnail", id: "itinerary-preview-image", alt: "", data: { static_map_url_builder: "http://maps.googleapis.com/maps/api/staticmap?size=200x200&sensor=false&markers=color:green|label:B|%{end_location}&markers=color:green|label:A|%{start_location}&path=enc:%{overview_polyline}" } + .span9 + %h1.pacifico#itinerary-preview-title + %table + %tbody + %tr + %td.profile-picture-cell.vertical-align-top + = user_profile_picture current_user, class: "inline-block" + %td.vertical-align-top + .balloon.for-profile-picture + %h5.no-margin= current_user + %p.pre-line#itinerary-preview-description + .spacer + .row-fluid + .span6 + %dl.dl-horizontal + %dt= Itinerary.human_attribute_name(:vehicle) + %dd#itinerary-preview-vehicle + %dt= Itinerary.human_attribute_name(:smoking_allowed) + %dd#itinerary-preview-smoking_allowed + %dt= Itinerary.human_attribute_name(:pets_allowed) + %dd#itinerary-preview-pets_allowed + %dt= Itinerary.human_attribute_name(:fuel_cost) + %dd + %span#itinerary-preview-fuel_cost< + %span>< .00 € + %dt= Itinerary.human_attribute_name(:tolls) + %dd + %span#itinerary-preview-tolls< + %span>< .00 € + .span6 + %dl.dl-horizontal + %dt= Itinerary.human_attribute_name(:round_trip) + %dd#itinerary-preview-round_trip + %dt= Itinerary.human_attribute_name(:leave_date) + %dd#itinerary-preview-leave_date + %dt.itinerary-preview-return.hide= Itinerary.human_attribute_name(:return_date) + %dd.itinerary-preview-return.hide#itinerary-preview-return_date +.row + .span12.align-center + .spacer + - if share_on_timeline_available + .control-group + .controls + = f.label :share_on_timeline, class: "checkbox inline" do + = f.default_tag :check_box, :share_on_timeline, disabled: !current_user.has_facebook_permission(:publish_stream), checked: current_user.has_facebook_permission(:publish_stream) + %i.icon-facebook-sign.facebook-color.icon-large + = Itinerary.human_attribute_name :share_on_timeline + - unless current_user.has_facebook_permission(:publish_stream) + %p.shy + %small + %i.icon-ban-circle + = t(".missing_publish_stream_permission") + - else + %i.icon-ban-circle + = t(".share_on_timeline_unavailable") diff --git a/app/views/itineraries/_details_step.html.haml b/app/views/itineraries/_details_step.html.haml new file mode 100644 index 000000000..c2a514ddf --- /dev/null +++ b/app/views/itineraries/_details_step.html.haml @@ -0,0 +1,88 @@ +.row + .span5 + = f.text_field :title, class: "input-xlarge width-max", maxlength: 40 + = f.select :vehicle, options_for_array_collection(Itinerary, :vehicle) + .control-group + .controls + = f.check_box :smoking_allowed, class: "checkbox inline" + = f.check_box :pets_allowed, class: "checkbox inline" + + .control-group{ class: ("error" if @itinerary.errors.include?(:fuel_cost)) } + = f.label :fuel_cost, class: "control-label" + .controls + .input-prepend.input-append + %span.add-on> + € + = f.default_tag :text_field, :fuel_cost, class: "span1 align-right" + %span.add-on> + \.00 + - if @itinerary.errors.include?(:fuel_cost) + %span.help-inline + = @itinerary.errors.messages[:fuel_cost].join(", ") + %p.help-block#fuel-help.hide{ data: { avg_consumption: current_user.vehicle_avg_consumption, text: t(".fuel_help_text", avg_consumption: current_user.vehicle_avg_consumption) } } + %small + %i.icon-info-sign + %span#fuel-help-text + = link_to t(".more_accurate_data"), :settings + + .control-group{ class: ("error" if @itinerary.errors.include?(:tolls)) } + = f.label :tolls, class: "control-label" + .controls + .input-prepend.input-append + %span.add-on> + € + = f.default_tag :text_field, :tolls, class: "input-mini align-right" + %span.add-on> + \.00 + - if @itinerary.errors.include?(:tolls) + %span.help-inline + = @itinerary.errors.messages[:tolls].join(", ") + .span7 + = f.text_area :description, rows: 5, class: "full-width border-box", help: true + .control-group.hide + .controls + = label_tag nil, class: "radio inline" do + = f.radio_button :recurrent, false, class: "inline", checked: true + = t(".single_itinerary") +   + = label_tag nil, class: "radio inline mock" do + = f.radio_button :recurrent, true, class: "inline" + = t(".daily_itinerary") + %fieldset#single + = f.check_box :round_trip + = f.datetime_select :leave_date, { start_year: Date.today.year, end_year: 1.year.from_now.year, prompt: true, minute_step: 10 }, class: "date-time" + = f.datetime_select :return_date, { start_year: Time.now.year, end_year: 1.year.from_now.year, prompt: true, minute_step: 10 }, class: "date-time", disabled: !@itinerary.round_trip + %fieldset#daily.hide.mock + .control-group + .controls + %table.table.table-striped.table-condensed.full-width#table-daily-itinerary + %thead + %tr + %th + %th + = t(".day") + %th + = t(".leave_time") + %th + = t(".return_time") + %tbody + - (0..6).each_with_index do |index| + %tr + %td{ style: "vertical-align: middle" } + %p.align-center + = check_box_tag "day-#{index}" + %td{ style: "vertical-align: middle" } + %p + = label_tag "day-#{index}", t("date.day_names")[index] + %td + = time_select "uni", "dye", { minute_step: 10, default: { hour: 8 } }, class: "date-time", disabled: (index > 0) + %td + = time_select "uni", "due", { minute_step: 10, default: { hour: 18 } }, class: "date-time", disabled: (index > 0) + %tfoot + %tr + %td{ colspan: 2 } + %td{ colspan: 2 } + %p + = label_tag "edit-all-times" do + = check_box_tag "edit-all-times", nil, checked: true + = t(".edit_all_times") diff --git a/app/views/itineraries/_route_step.html.haml b/app/views/itineraries/_route_step.html.haml new file mode 100644 index 000000000..f869009c1 --- /dev/null +++ b/app/views/itineraries/_route_step.html.haml @@ -0,0 +1,45 @@ +.row + .span3 + = bootstrap_form_for ItineraryRoute.new, url: "#", method: "get", validate: true, html: { data: { not_found_text: t(".not_found_text"), zero_results_text: t(".zero_results_text"), get_route_before_text: t(".get_route_before") } } do |f| + = f.text_field :from, class: "input-xlarge width-max" + = f.text_field :to, class: "input-xlarge width-max" + = f.check_box :avoid_highways + = f.check_box :avoid_tolls + = f.submit t(".get_route"), class: "btn btn-primary" + = image_tag 'ajax-spinner-24x17.gif', width: 24, height: 17, alt: "...", id: "itineraries-spinner", class: "hide" + #error.alert.alert-error + #result.alert.alert-info + #{t(".distance")}: + %b#distance + %br + #{t(".duration")}: + %b#duration + .span9 + #new-itinerary-map.google-maps-fix + %p#route-helper.hide + %small + Da: + %span#from-helper + a: + %span#to-helper +.row + .span12 + .spacer + .span4 + %h3.no-margin + %i.icon-eye-close + = t(".hint_privacy_title") + %p + = t(".hint_privacy_content") + .span4 + %h3.no-margin + %i.icon-wrench + = t(".hint_customize_title") + %p + = t(".hint_customize_content") + .span4 + %h3.no-margin + %i.icon-search + = t(".hint_search_before_title") + %p + = t(".hint_search_before_content", search_before_link: (link_to t(".search_before_link"), itineraries_path)).html_safe diff --git a/app/views/itineraries/_search.html.haml b/app/views/itineraries/_search.html.haml new file mode 100644 index 000000000..139417309 --- /dev/null +++ b/app/views/itineraries/_search.html.haml @@ -0,0 +1,29 @@ += bootstrap_form_for ItinerarySearch.new, url: search_itineraries_path, method: :post, validate: true, remote: true do |f| + = f.text_field :from, class: "input-xlarge width-max", label: ItineraryRoute.human_attribute_name(:from) + = f.text_field :to, class: "input-xlarge width-max", label: ItineraryRoute.human_attribute_name(:to) + %p + = link_to "#", id: "search-form-advanced-link" do + %i.icon-chevron-up + = t(".advanced_search") + #search-form-advanced.hide.form-inline + %table + - [:smoking_allowed, :pets_allowed, :round_trip].each do |boolean_field| + %tr + %td>< + = f.label "filter_#{boolean_field}".to_sym, Itinerary.human_attribute_name(boolean_field) + = ":" + %td.select-field + = f.default_tag :select, "filter_#{boolean_field}".to_sym, boolean_options_for_select, { include_blank: true } + %tr + %td>< + = f.label :filter_driver_gender, Itinerary.human_attribute_name(:driver_gender) + = ":" + %td.select-field + = f.default_tag :select, :filter_driver_gender, options_for_array_collection(User, :gender), { include_blank: true }, class: "width-auto" + + = f.hidden_field :start_location_lat + = f.hidden_field :start_location_lng + = f.hidden_field :end_location_lat + = f.hidden_field :end_location_lng + = button_tag t(".search"), type: "button", class: "btn btn-primary", id: "itineraries-search" + = image_tag 'ajax-spinner-24x17.gif', width: 24, height: 17, alt: "...", id: "itineraries-spinner", class: "hide" \ No newline at end of file diff --git a/app/views/itineraries/edit.html.haml b/app/views/itineraries/edit.html.haml new file mode 100644 index 000000000..c862517d9 --- /dev/null +++ b/app/views/itineraries/edit.html.haml @@ -0,0 +1,5 @@ +- title t(".title") +.row + .span12 + = render "shared/flash_messages" + %h1= yield :title diff --git a/app/views/itineraries/index.html.haml b/app/views/itineraries/index.html.haml new file mode 100644 index 000000000..0e51effc4 --- /dev/null +++ b/app/views/itineraries/index.html.haml @@ -0,0 +1,16 @@ +- title t(".title") +- content_for :footer do + #translations{ data: { no_itineraries_found: t(".no_itineraries_found"), + an_error_occurred: t(".an_error_occurred"), + read_more: t("templates.itineraries.read_more"), + show_on_map: t("templates.itineraries.show_on_map") } } +.row + .span3 + %h3= yield :title + = render "search" + #error.alert.alert-error + .span9 + #index-itineraries-map.google-maps-fix + .spacer + .row-fluid + #itineraries-thumbs diff --git a/app/views/itineraries/mine.html.haml b/app/views/itineraries/mine.html.haml new file mode 100644 index 000000000..f0a20a6a9 --- /dev/null +++ b/app/views/itineraries/mine.html.haml @@ -0,0 +1,26 @@ +- title t(".title") +.row + .span12 + = render "shared/flash_messages" + %h1= yield :title + .span12 + %table.table.table-striped + %thead + %th # + %th= Itinerary.human_attribute_name(:title) + %th= t("helpers.actions") + %tbody + - @itineraries.each_with_index do |itinerary, index| + %tr.vertical-align-middle + %td + = index.next + %td + = link_to itinerary, itinerary_path(itinerary) + %td + = link_to edit_itinerary_path(itinerary), class: "btn btn-small mock" do + %i.icon-edit + = t("helpers.links.edit") +   + = link_to itinerary_path(itinerary), method: :delete, data: { confirm: t("helpers.links.confirm") }, class: "btn btn-small btn-danger" do + %i.icon-trash + = t("helpers.links.destroy") diff --git a/app/views/itineraries/new.html.haml b/app/views/itineraries/new.html.haml new file mode 100644 index 000000000..812d95f68 --- /dev/null +++ b/app/views/itineraries/new.html.haml @@ -0,0 +1,40 @@ +- title t(".title") +- if @itinerary.errors.any? + .row + .span12 + .alert.alert-block.alert-error.fade.in + %button{ type: "button", class: "close", data: { dismiss: "alert" } } × + %h4.alert-heading= t("errors.template.header", model: Itinerary.model_name.human, count: @itinerary.errors.size) + = t("errors.template.body") + %ul + - @itinerary.errors.full_messages.each do |message| + %li= message +.row + #wizard-step-1-title.span4.itinerary-step.active + %h2.no-margin + %i.icon-check-empty.hidden-phone + = t(".step_1") + %p= t(".plan_route") + #wizard-step-2-title.span4.itinerary-step.hidden-phone + %h2.no-margin + %i.icon-check-empty.hidden-phone + = t(".step_2") + %p= t(".insert_itinerary_details") + #wizard-step-3-title.span4.itinerary-step.hidden-phone + %h2.no-margin + %i.icon-check-empty.hidden-phone + = t(".step_3") + %p= t(".confirm_and_share") +.spacer +#wizard-step-1-content + = render "route_step" += bootstrap_form_for @itinerary, validate: true, html: { data: { step: 1, :"last-step" => 3 } } do |f| + #wizard-step-2-content.hide + = render "details_step", f: f + #wizard-step-3-content.hide + = render "confirm_and_share_step", f: f + .form-actions.pagination-centered + = f.hidden_field :route_json_object + = f.submit t(".confirm_and_share"), class: "btn btn-large btn-success hide", id: "new_itinerary_submit", disabled: true + = f.submit t(".next_step"), type: "button", class: "btn btn-large btn-primary", name: "next_button", id: "wizard-next-step-button", disabled: false + = button_tag t(".previous_step"), type: "button", class: "btn btn-large hide", name: "back_button", id: "wizard-prev-step-button", disabled: true diff --git a/app/views/itineraries/search.json.rabl b/app/views/itineraries/search.json.rabl new file mode 100644 index 000000000..1b840ec08 --- /dev/null +++ b/app/views/itineraries/search.json.rabl @@ -0,0 +1,3 @@ +collection @itineraries + +extends 'itineraries/show' diff --git a/app/views/itineraries/show.html.haml b/app/views/itineraries/show.html.haml new file mode 100644 index 000000000..14394deba --- /dev/null +++ b/app/views/itineraries/show.html.haml @@ -0,0 +1,89 @@ +- title @itinerary.title += content_for :head do + = tag :meta, property: "fb:app_id", content: APP_CONFIG.facebook.app_id + = tag :meta, property: "og:url", content: itinerary_url(@itinerary) + = tag :meta, property: "og:site_name", content: APPNAME + = tag :meta, property: "og:type", content: "codenameicare:itinerary" + = tag :meta, property: "og:title", content: @itinerary.title + = tag :meta, property: "og:image", content: @itinerary.static_map + = tag :meta, property: "og:description", content: @itinerary.description + = tag :meta, property: "codenameicare:route_start_location:latitude", content: @itinerary.start_location["lat"] + = tag :meta, property: "codenameicare:route_start_location:longitude", content: @itinerary.start_location["lng"] + = tag :meta, property: "codenameicare:route_end_location:latitude", content: @itinerary.end_location["lat"] + = tag :meta, property: "codenameicare:route_end_location:longitude", content: @itinerary.end_location["lng"] + - @itinerary.sample_path.each do |point| + = tag :meta, property: "codenameicare:route_sample_path:latitude", content: point[0] + = tag :meta, property: "codenameicare:route_sample_path:longitude", content: point[1] +.row + .span3 + = image_tag @itinerary.static_map, class: "thumbnail border-box", alt: "" + .span9 + %h1.pacifico + = @itinerary.title || Itinerary.model_name.human + %table + %tbody + %tr + %td.profile-picture-cell.vertical-align-top + - if logged_in? + = user_profile_picture @itinerary.user, class: "inline-block" + - else + = image_tag "http://fbcdn-profile-a.akamaihd.net/static-ak/rsrc.php/v2/yo/r/UlIqmHJn-SK.gif", width: 50, height: 50, class: "inline-block thumbnail", alt: "" + %td.vertical-align-top + .balloon.for-profile-picture + - if logged_in? + %h5.no-margin= link_to @itinerary.user, user_path(@itinerary.user) + %p.pre-line= @itinerary.description + - else + %h4= t(".app_user", appname: APPNAME) + = @itinerary.description + .spacer + .row-fluid + .span6 + %dl.dl-horizontal + %dt= Itinerary.human_attribute_name(:vehicle) + %dd= Itinerary.human_attribute_name("vehicle_#{@itinerary.vehicle}") + %dt= Itinerary.human_attribute_name(:smoking_allowed) + %dd= t("boolean.#{@itinerary.smoking_allowed}") + %dt= Itinerary.human_attribute_name(:pets_allowed) + %dd= t("boolean.#{@itinerary.pets_allowed}") + %dt= Itinerary.human_attribute_name(:fuel_cost) + %dd + %span= @itinerary.fuel_cost + %span>< .00 € + %dt= Itinerary.human_attribute_name(:tolls) + %dd + %span= @itinerary.tolls + %span>< .00 € + .span6 + %dl.dl-horizontal + %dt= Itinerary.human_attribute_name(:round_trip) + %dd= t("boolean.#{@itinerary.round_trip}") + %dt= Itinerary.human_attribute_name(:leave_date) + %dd= l(@itinerary.leave_date, format: :long) + - if @itinerary.round_trip + %dt.itinerary-preview-return= Itinerary.human_attribute_name(:return_date) + %dd.itinerary-preview-return= l(@itinerary.return_date, format: :long) + .row-fluid + .span12 + - if logged_in? + - if current_user != @itinerary.user + .btn-toolbar + = link_to new_conversation_path(itinerary_id: @itinerary.id), class: "btn btn-primary" do + %i.icon-comment + = t(".contact", user: @itinerary.user_first_name) + = link_to "#", class: "btn mock" do + %i.icon-thumbs-up + = t(".leave_reference") + .btn-group + %button.btn.btn-warning + %i.icon-flag + %button.btn.btn-warning.dropdown-toggle{ data: { toggle: "dropdown" } } + %span.caret + %ul.dropdown-menu + %li + %a{ href: "#" } + = t(".report_itinerary") + - else + = link_to root_path, class: "btn btn-facebook" do + %i.icon-facebook-sign.icon-large + = t(".login_to_contact") diff --git a/app/views/itineraries/show.json.rabl b/app/views/itineraries/show.json.rabl new file mode 100644 index 000000000..be681409a --- /dev/null +++ b/app/views/itineraries/show.json.rabl @@ -0,0 +1,11 @@ +object @itinerary + +attributes :id, :description, :end_location, :fuel_cost, :leave_date, :num_people, :overview_polyline, + :pets_allowed, :recurrent, :return_date, :round_trip, :smoking_allowed, :start_location, + :title, :tolls, :vehicle, :via_waypoints + +node(:url) { |itinerary| itinerary_url(itinerary) } + +child :user do + attributes :name, :uid, :nationality, :to_param, :profile_picture +end diff --git a/app/views/kaminari/_first_page.html.haml b/app/views/kaminari/_first_page.html.haml new file mode 100755 index 000000000..50b260685 --- /dev/null +++ b/app/views/kaminari/_first_page.html.haml @@ -0,0 +1,14 @@ +-# + Link to the "First" page + available local variables + url: url to the first page + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + +%li{ class: ("disabled" if current_page.first?) } + - if current_page.first? + = link_to raw(t 'views.pagination.first'), "#" + - else + = link_to raw(t 'views.pagination.first'), url, remote: remote diff --git a/app/views/kaminari/_gap.html.haml b/app/views/kaminari/_gap.html.haml new file mode 100755 index 000000000..97aafa425 --- /dev/null +++ b/app/views/kaminari/_gap.html.haml @@ -0,0 +1,10 @@ +-# + Non-link tag that stands for skipped pages... + available local variables + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + +%li.page.gap.disabled + = link_to raw(t 'views.pagination.truncate'), "#" diff --git a/app/views/kaminari/_last_page.html.haml b/app/views/kaminari/_last_page.html.haml new file mode 100755 index 000000000..4a7e175ee --- /dev/null +++ b/app/views/kaminari/_last_page.html.haml @@ -0,0 +1,14 @@ +-# + Link to the "Last" page + available local variables + url: url to the last page + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + +%li{ class: ("disabled" if current_page.last?) } + - if current_page.last? + = link_to raw(t 'views.pagination.last'), "#" + - else + = link_to raw(t 'views.pagination.last'), url, remote: remote diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100755 index 000000000..de024a9b7 --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,14 @@ +-# + Link to the "Next" page + available local variables + url: url to the next page + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + +%li.next{ class: ("disabled" if current_page.last?) } + - if current_page.last? + = link_to raw(t 'views.pagination.next'), "#" + - else + = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/_page.html.haml b/app/views/kaminari/_page.html.haml new file mode 100755 index 000000000..ad1ec27b2 --- /dev/null +++ b/app/views/kaminari/_page.html.haml @@ -0,0 +1,12 @@ +-# + Link showing page number + available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + +%li.page{ class: ("active" if page.current?) } + = link_to page, url, opts = { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100755 index 000000000..5e6a802f6 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,21 @@ +-# + The container tag + available local variables + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside + += paginator.render do + .pagination.pagination-centered + %ul + = first_page_tag + = prev_page_tag + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag + = last_page_tag diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100755 index 000000000..76f24d4c2 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,14 @@ +-# + Link to the "Previous" page + available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + num_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + +%li.previous{ class: ("disabled" if current_page.first?) } + - if current_page.first? + = link_to raw(t 'views.pagination.previous'), "#" + - else + = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml new file mode 100644 index 000000000..23056f03c --- /dev/null +++ b/app/views/layouts/application.html.haml @@ -0,0 +1,34 @@ +!!! +%html{lang: I18n.locale.to_s} + %head{ prefix: "og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# codenameicare: http://ogp.me/ns/fb/codenameicare#" } + %meta{ charset: "utf-8" } + %meta{ name: "viewport", content: "width=device-width, initial-scale=1.0" } + %title= yield_or_default :title, controller.action_name.titlecase + = csrf_meta_tags + = yield :head + / Le HTML5 shim, for IE6-8 support of HTML elements + /[if lt IE 9] + + + / Le styles + = stylesheet_link_tag "http://fonts.googleapis.com/css?family=Oxygen", "http://fonts.googleapis.com/css?family=Pacifico", "application", media: "all" + + / Le fav and touch icons + %link{ href: "/assets/favicon.ico", rel: "shortcut icon" } + %link{ href: "/assets/apple-touch-icon.png", rel: "apple-touch-icon" } + %link{ href: "/assets/apple-touch-icon-72x72.png", rel: "apple-touch-icon", sizes: "72x72" } + %link{ href: "/assets/apple-touch-icon-114x114.png", rel: "apple-touch-icon", sizes: "114x114" } + + %body + = render "shared/navbar" + #main-container.container + .content + = yield + %hr + = render "shared/footer" + / /container + / + Le javascript + \================================================== + / Placed at the end of the document so the pages load faster + = javascript_include_tag "http://maps.googleapis.com/maps/api/js?sensor=true&libraries=geometry", "application" diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml new file mode 100644 index 000000000..e5cda6cd9 --- /dev/null +++ b/app/views/notifications/index.html.haml @@ -0,0 +1,35 @@ +- title t(".title") +.row + .span12 + %h1= yield :title + .span12 + %table.table.table-striped + %tbody + - @notifications.each do |notification| + %tr + %td{ class: ("notification-unread" if notification.unread?) } + = notification_icon(notification) + = notification_message(notification) + %td.align-right{ class: ("notification-unread" if notification.unread?) } + %p + %small + %abbr{ title: l(notification.created_at.in_time_zone(current_user.time_zone), format: :long) } + #{distance_of_time_in_words(notification.created_at, Time.now.utc, true)} #{t("time_ago")} + .span12 + .pagination.pagination-centered.hide + %ul + %li.disabled + %a{ href: "#" } + « + %li.active + %a 1 + %li + %a{ href: "#" } 2 + %li.disabled + %a{ href: "#" } … + %li + %a{ href: "#" } 8 + %li + %a{ href: "#" } 9 + %li + %a{ href: "#" } » diff --git a/app/views/pages/about.en-US.html.haml b/app/views/pages/about.en-US.html.haml new file mode 100644 index 000000000..17cfabead --- /dev/null +++ b/app/views/pages/about.en-US.html.haml @@ -0,0 +1,6 @@ +- title t("about") +.row + .span12 + %h1= yield(:title) + %p + In English diff --git a/app/views/pages/about.it-IT.html.haml b/app/views/pages/about.it-IT.html.haml new file mode 100644 index 000000000..598c866a4 --- /dev/null +++ b/app/views/pages/about.it-IT.html.haml @@ -0,0 +1,6 @@ +- title t("about") +.row + .span12 + %h1= yield(:title) + %p + In Italiano diff --git a/app/views/pages/demo_terms.en-US.html.haml b/app/views/pages/demo_terms.en-US.html.haml new file mode 100644 index 000000000..cd7ef9f51 --- /dev/null +++ b/app/views/pages/demo_terms.en-US.html.haml @@ -0,0 +1,33 @@ +- title t(".title") +.row + .span12 + %h1= yield(:title) + %h5.shy= t("last_modified", date: l(Date.parse("2012-09-01"), format: :long)) + + %h3 Welcome to #{APPNAME} + %p + This is a demo application of the open source + = link_to "carpooling", "http://en.wikipedia.org/wiki/Carpool" + platform #{APPNAME}. + %p + The source code is available at + = link_to "http://github.com/diowa/icare", "http://github.com/diowa/icare" + + %h3 Our Warranties and Disclaimers + %p + WE PROVIDE THIS DEMO "AS IS", WITHOUT ANY WARRANTY. + + %h3 Privacy policy + %p + We cache the following Facebook data on our local database for performance reasons: + %ul + %li + Email, name, birthday, work, education, about, total number of friends, known languages. + We get your public profile picture, likes and common friends with other #{APPNAME}'s members without + storing them in any way. Your username on #{APPNAME} will be the same of Facebook. + %p + We will not send you any email. + %p + The "Share on Facebook Timeline" feature is not available in this demo. + %p + You can permanently delete your data by logging in and clicking the "Delete account" button on the footer. diff --git a/app/views/pages/fbjssdk_channel.html.haml b/app/views/pages/fbjssdk_channel.html.haml new file mode 100644 index 000000000..759c73ef7 --- /dev/null +++ b/app/views/pages/fbjssdk_channel.html.haml @@ -0,0 +1 @@ += javascript_include_tag "http://connect.facebook.net/en_US/all.js" \ No newline at end of file diff --git a/app/views/pages/home.html.haml b/app/views/pages/home.html.haml new file mode 100644 index 000000000..117a4984a --- /dev/null +++ b/app/views/pages/home.html.haml @@ -0,0 +1,66 @@ +- title APPNAME +#fb-root += render "shared/flash_messages" +.row + .span12 + .hero-unit#home-hero + .row-fluid + .span4#home-picture + .thumbnail + = link_to "#" do + = image_tag 'http://placehold.it/640x480/2d7489/&text=how+it+works' + .span8 + %h1.pacifico + = image_tag "turtle_black.svg", class: "home-hero-logo" + = APPNAME + - if APP_CONFIG.demo_mode + %small + (demo) + %p + %b= t('.tagline', appname: APPNAME) + .row-fluid + .span6 + .fb-facepile{ data: { size: "medium", :"max-rows" => "1" } } + .span6 + %p + = link_to auth_at_provider_path(provider: :facebook), class: "btn btn-large btn-facebook" do + %i.icon-facebook-sign.icon-large + = t("login_with_facebook") + %small.why-facebook + = link_to t("why_facebook"), "#why-facebook", data: { toggle: "modal" } + %p.signin-terms + %b= t("we_will_never_post") + %br + - if APP_CONFIG.demo_mode + = t(".demo_mode", demo_terms_link: link_to(t("demo_terms"), :demo_terms)).html_safe + - else + = t(".signin_terms", appname: APPNAME, terms_link: link_to(t("terms"), :terms), policy_link: link_to(t("policy"), :policy)).html_safe +.row + .span4 + %h2.home-h2 + %i.icon-road + = t('.share_itinerary_title') + %p= t('.share_itinerary_content') + .span4 + %h2.home-h2 + %i.icon-group + = t('.make_new_connections_title') + %p= t('.make_new_connections_content') + .span4 + %h2.home-h2 + %i.icon-money + = t('.save_money_title') + %p= t('.save_money_content') +.row + .span4 + %h2.home-h2 + %i.icon-magic + = t('.thats_easy_title') + %p= t('.thats_easy_content') + .span4 + %h2.home-h2 + %i.icon-leaf + = t('.help_environment_title') + %p= t('.help_environment_content') + += render "shared/modal_why_facebook" diff --git a/app/views/pages/how_it_works.en-US.html.haml b/app/views/pages/how_it_works.en-US.html.haml new file mode 100644 index 000000000..8d169b040 --- /dev/null +++ b/app/views/pages/how_it_works.en-US.html.haml @@ -0,0 +1,6 @@ +- title t("how_it_works") +.row + .span12 + %h1= yield(:title) + %p + In English diff --git a/app/views/pages/how_it_works.it-IT.html.haml b/app/views/pages/how_it_works.it-IT.html.haml new file mode 100644 index 000000000..6f8797b3f --- /dev/null +++ b/app/views/pages/how_it_works.it-IT.html.haml @@ -0,0 +1,6 @@ +- title t("how_it_works") +.row + .span12 + %h1= yield(:title) + %p + In Italiano diff --git a/app/views/pages/policy.en-US.html.haml b/app/views/pages/policy.en-US.html.haml new file mode 100644 index 000000000..b903b16d5 --- /dev/null +++ b/app/views/pages/policy.en-US.html.haml @@ -0,0 +1,82 @@ +- title t(".title") +.row + .span12 + %h1= yield(:title) + %h5.shy= t("last_modified", date: l(Date.parse("2012-09-01"), format: :long)) + + %h3 Information we collect + %p + We collect some information to provide better services to all of our users. + %p + We collect information in two ways: + %ul + %li + %b Information you give us. + In the profile settings, we’ll ask for personal information, like your nationality and your vehicle's average consumption. + %li + %b Information we get from Facebook. + This information includes: + %ul + %li + Public profile picture, email, name, birthday, work, education, total number of friends, spoken languages: all these values are cached in our databases. + %li + Likes and common friends with other icare users: these values are only showed when viewing a profile. + %h3 How we use information we collect + %p + We use the information we collect on our service to provide, maintain, protect and improve it. + + icare processes personal information on our servers in many countries around the world. We may process your personal information on a server located outside the country where you live. + + %h3 Transparency and choice + %p + People have different privacy concerns. Our goal is to be clear about what information we collect, + so that you can make meaningful choices about how it is used. For example, you can: + %ul + %li + = link_to_if logged_in?, "Delete", :settings + your account + You may also set your browser to block all cookies, including cookies associated with our services, or to indicate + when a cookie is being set by us. However, it’s important to remember that many of our services may not function + properly if your cookies are disabled. For example, we may not remember your language preferences. + + %h3 Information you share + %p + Our service let you share information with others. Remember that when you share information publicly, + it may be indexable by search engines. + + %h3 Accessing and updating your personal information + %p + You can edit information you provide us through profile settings. You can't edit information we retrieve from + Facebook because these are only cached on our database for performance reasons. If you need to edit those information, + edit them on Facebook and log in again on our service. + + %h3 Information we share + %p + We do not share personal information with companies, organizations and individuals outside of #{APPNAME} unless one of the following circumstances apply: + %ul + %li + %b For legal reasons + We will share personal information with companies, organizations or individuals outside of #{APPNAME} if we have a good-faith belief that access, use, preservation or disclosure of the information is reasonably necessary to: + %ul + %li meet any applicable law, regulation, legal process or enforceable governmental request. + %li enforce applicable Terms of Service, including investigation of potential violations. + %li detect, prevent, or otherwise address fraud, security or technical issues. + %li protect against harm to the rights, property or safety of #{APPNAME}, our users or the public as required or permitted by law. + %p + We may share aggregated, non-personally identifiable information publicly and with our partners – like publishers, advertisers or connected sites. For example, we may share information publicly to show trends about the general use of our services. + %p + If #{APPNAME} is involved in a merger, acquisition or asset sale, we will continue to ensure the confidentiality of any personal information and give affected users notice before personal information is transferred or becomes subject to a different privacy policy. + + %h3 Application + %p + Our Privacy Policy does not apply to services offered by other companies or individuals, + including products or sites that may be linked from our service. Our Privacy Policy does + not cover the information practices of other companies and organizations who advertise our services, + and who may use cookies, pixel tags and other technologies to serve and offer relevant ads. + + %h3 Changes + %p + Our Privacy Policy may change from time to time. We will not reduce your rights under this + Privacy Policy without your explicit consent. We will post any privacy policy changes on this page and, + if the changes are significant, we will provide a more prominent notice (including, for certain services, + email notification of privacy policy changes). diff --git a/app/views/pages/policy.it-IT.html.haml b/app/views/pages/policy.it-IT.html.haml new file mode 100644 index 000000000..ca06ac91f --- /dev/null +++ b/app/views/pages/policy.it-IT.html.haml @@ -0,0 +1,6 @@ +- title t("policy") +.row + .span12 + %h1= yield(:title) + %p + In Italiano diff --git a/app/views/pages/terms.en-US.html.haml b/app/views/pages/terms.en-US.html.haml new file mode 100644 index 000000000..46c1ddc8a --- /dev/null +++ b/app/views/pages/terms.en-US.html.haml @@ -0,0 +1,42 @@ +- title t(".title") +.row + .span12 + %h1= yield(:title) + %h5.shy= t("last_modified", date: l(Date.parse("2012-09-01"), format: :long)) + + %h3 Welcome to #{APPNAME} + %p + Thanks for using our service (“Service”). + %p + By using our Service you are agreeing to these terms. Please read them carefully. + + %h3 Using our Service + %p + This is a demo application of the open source carpooling platform #{APPNAME}. + %p + The source code is available at + = link_to "http://github.com/diowa/icare", "http://github.com/diowa/icare" + + %h3 Privacy Protection + %p + #{APPNAME}'s + = link_to "privacy policy", :policy + explain how we treat your personal data and protect your privacy when you use our Service. + %p + By using our Service, you agree that #{APPNAME} can use such data in accordance with our privacy policy. + + %h3 Modifying and Terminating our Service + %p + We are constantly changing and improving our Service. + %p + You can stop using this Service at any time. + %p + We may also stop providing this Service to you, or add or create new limits to this Service at any time. + + %h3 Our Warranties and Disclaimers + %p + WE DON'T MAKE ANY PROMISE ABOUT THIS SERVICE. FOR EXAMPLE, WE DON’T MAKE ANY COMMITMENTS + ABOUT THE CONTENT WITHIN THIS SERVICE, THE SPECIFIC FUNCTION OF THIS SERVICE, OR ITS RELIABILITY, + AVAILABILITY, OR ABILITY TO MEET YOUR NEEDS. WE PROVIDE THIS SERVICE “AS IS”. + %p + WE EXCLUDE ALL WARRANTIES. diff --git a/app/views/pages/terms.it-IT.html.haml b/app/views/pages/terms.it-IT.html.haml new file mode 100644 index 000000000..2656132f5 --- /dev/null +++ b/app/views/pages/terms.it-IT.html.haml @@ -0,0 +1,6 @@ +- title t("terms") +.row + .span12 + %h1= yield(:title) + %p + In Italiano diff --git a/app/views/references/_form.html.haml b/app/views/references/_form.html.haml new file mode 100644 index 000000000..397732ab3 --- /dev/null +++ b/app/views/references/_form.html.haml @@ -0,0 +1,31 @@ +- unless @reference.outgoing_reference.relevant + .alert.alert-warning.fade.in + %a.close{"data-dismiss" => "alert", href: "#"} × + %h4= t(".warning") + %p= t(".marked_as_not_relevant", mark_as_relevant: t(".mark_as_relevant")) +%p + = reference_status_for(@reference) +%p.help + = reference_help_text_for(@reference) += bootstrap_form_for @reference, validate: true, html: { class: 'form-vertical' } do |f| + = f.fields_for :outgoing_reference do |outgoing_reference| + = outgoing_reference.error_messages + = outgoing_reference.text_area :message, class: 'text_field', rows: 10, help: true, class: "width-max border-box", disabled: ("disabled" unless @reference.outgoing_reference.relevant) + .control-group + .controls + = outgoing_reference.hidden_field :rating, value: @reference.outgoing_reference.rating + .btn-group{ data: { toggle: "buttons-radio" } } + %button.btn.reference-rating{ type: "button", data: { rating: "-1" }, class: ("active" if @reference.outgoing_reference.rating == -1) } + %i.icon-thumbs-down + = t(".negative") + %button.btn.reference-rating{ type: "button", data: { rating: "0" }, class: ("active" if @reference.outgoing_reference.rating == 0) } + = t(".neutral") + %button.btn.reference-rating{ type: "button", data: { rating: "1" }, class: ("active" if @reference.outgoing_reference.rating == 1) } + %i.icon-thumbs-up + = t(".positive") + .form-actions + - if @reference.outgoing_reference.relevant + = f.submit t(".send_reference"), class: 'btn btn-primary' + = link_to t(".mark_as_not_relevant"), not_relevant_incoming_reference_path(@reference), class: "btn", method: :put + - else + = link_to t(".mark_as_relevant"), relevant_incoming_reference_path(@reference), class: "btn btn-primary", method: :put diff --git a/app/views/references/_pending_references.html.haml b/app/views/references/_pending_references.html.haml new file mode 100644 index 000000000..5a769c538 --- /dev/null +++ b/app/views/references/_pending_references.html.haml @@ -0,0 +1,42 @@ +%table.table.table-striped.table-in-calendar + - if references.empty? + %p.grayed-out + You have no pending references + - else + %thead + %tr + %th.width-min.no-border-top + = t(".type") + %th.width-min.no-border-top + = t(".user") + %th.width-min.nowrap.no-border-top + = t(".users_rating") + %th.no-border-top + = t(".users_message_snippet") + + %tbody + - references.each do |reference| + %tr{ class: ("not-relevant-for-me" unless reference.outgoing_reference.relevant) } + %td.align-center + - if reference.is_guest + %i.titleable-icon.icon-plane{ title: t("users.calendar_travelling.travelling") } + -else + %i.titleable-icon.icon-home{ title: t("users.calendar_hosting.hosting") } + %td + = link_to edit_incoming_reference_path(reference), class: "user-tag" do + = small_user_avatar reference.referencing_user, false, "micro" + %p.name + = reference.referencing_user + - if reference.filled_up? + %td + = make_thumbs reference.rating + %td + #{reference.message[0...50]}... + - else + %td{ colspan: 2 } + - if reference.relevant + %i.grayed-out + = t("references.edit.other_did_not_write", name: reference.referencing_user) + - else + %i.grayed-out + = t("references.edit.other_not_relevant", name: reference.referencing_user) diff --git a/app/views/references/edit.html.haml b/app/views/references/edit.html.haml new file mode 100644 index 000000000..274b01cae --- /dev/null +++ b/app/views/references/edit.html.haml @@ -0,0 +1,21 @@ +- model_class = @reference.experience_class.constantize +.row + .span6 + %h1.legend-like= t('.send_reference') + = render "form" + .span6 + %h1.legend-like= @reference.referencing_user + - if @reference.filled_up? + %h1 + = make_thumbs(@reference.rating) + .div{ style: "display: table-cell; width: 80px; vertical-align: middle" } + = small_user_avatar @reference.referencing_user, true, "small" + .div{ style: "display: table-cell;" } + %p.balloon + = @reference.message + - elsif !@reference.relevant + %p.grayed-out + = t(".other_not_relevant", name: @reference.referencing_user) + - else + %p.grayed-out + = t(".other_did_not_write", name: @reference.referencing_user) diff --git a/app/views/shared/_flash_messages.html.haml b/app/views/shared/_flash_messages.html.haml new file mode 100644 index 000000000..fad6ddb42 --- /dev/null +++ b/app/views/shared/_flash_messages.html.haml @@ -0,0 +1,6 @@ +-unless flash.empty? + - flash.each do |type, message| + .alert.fade.in{ class: "alert-#{twitterized_type(type)}" } + %btn.close{ type: "button", data: { dismiss: "alert"} } + × + = message diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml new file mode 100644 index 000000000..6834abbf6 --- /dev/null +++ b/app/views/shared/_footer.html.haml @@ -0,0 +1,25 @@ +%footer + %ul.unstyled + %li + = image_tag transparent_gif_image_data, alt: "diowa", height: 25, width: 80, title: "diowa", class: "diowa-logo" + - if logged_in? + %li= link_to t("about"), :about + %li= link_to t("how_it_works"), :how_it_works + - unless APP_CONFIG.demo_mode + %li= link_to t("terms"), :terms + %li + = link_to feedbacks_url, class: "btn btn-info btn-mini" do + %i.icon-bullhorn + = Feedback.model_name.human(count: :lot) + %li + = link_to user_path(current_user), method: :delete, class: "btn btn-danger btn-mini", data: { confirm: t("helpers.links.confirm") } do + %i.icon-remove + = t("delete_account") + %li + %i.icon-github + = link_to "diowa/icare", "http://github.com/diowa/icare" + - if APP_CONFIG.demo_mode + %li + %i.icon-warning-sign + = t("demo_mode") + = yield :footer \ No newline at end of file diff --git a/app/views/shared/_modal_why_facebook.en-US.html.haml b/app/views/shared/_modal_why_facebook.en-US.html.haml new file mode 100644 index 000000000..d19245ec9 --- /dev/null +++ b/app/views/shared/_modal_why_facebook.en-US.html.haml @@ -0,0 +1,24 @@ +.modal.fade.in.hide#why-facebook + .modal-header + %button.close{ type: "button", data: { dismiss: "modal" } } + × + %h3= t("why_facebook") + .modal-body + %p + We know that some people do not use Facebook or do not allow permissions to third party applications. + %p + Anyway, the creation of a new profile will you solve a captcha, wait for an activation email, insert + personal data, upload at least one picture, select an user name, remember a new password, etc. + Logging in by Facebook you can skip this boring process. + %p + Personal identity matters for our application. You can trust in any way your new travel companions and Facebook provides + a first verification through friends, friends of friends, uploaded photos... + %p + Moreover, on Facebook we can find travel companions who share your same interests. + %p + %i.icon-info-sign.icon-large + %u + %strong + NOTE: We will never post anything without your explicit consent. + .modal-footer + = link_to t("close"), "#", data: { dismiss: "modal" }, class: "btn" diff --git a/app/views/shared/_modal_why_facebook.it-IT.html.haml b/app/views/shared/_modal_why_facebook.it-IT.html.haml new file mode 100644 index 000000000..dab4f74fb --- /dev/null +++ b/app/views/shared/_modal_why_facebook.it-IT.html.haml @@ -0,0 +1,23 @@ +.modal.fade.in.hide#why-facebook + .modal-header + %button.close{ type: "button", data: { dismiss: "modal" } } + × + %h3= t("why_facebook") + .modal-body + %p + Siamo consapevoli che qualcuno non utilizza Facebook oppure non concede volentieri i permessi alle applicazioni di terze parti. + %p + La creazione di un nuovo profilo, tuttavia, ti constringerebbe a risolvere un captcha, attendere la + mail di attivazione, inserire dati anagrafici, caricare almeno una foto, + scegliere un nome utente, ricordare una nuova password, ecc. Accedendo con Facebook è possibile evitare questa noiosa procedura. + %p + Inoltre è facile immaginare che l'identità personale per una applicazione come la nostra è fondamentale. Bisogna potersi fidare in qualche modo dei nuovi compagni di viaggio e Facebook permette una prima verifica tramite amici, amici di amici, foto caricate... + %p + Su Facebook, infine, possiamo evidenziare i compagni di viaggio più adatti a te grazie all'analisi degli interessi comuni. + %p + %i.icon-info-sign.icon-large + %u + %strong + NOTA: Non posteremo mai nulla senza il tuo esplicito permesso. + .modal-footer + = link_to t("close"), "#", data: { dismiss: "modal" }, class: "btn" diff --git a/app/views/shared/_navbar.html.haml b/app/views/shared/_navbar.html.haml new file mode 100644 index 000000000..58110950d --- /dev/null +++ b/app/views/shared/_navbar.html.haml @@ -0,0 +1,69 @@ +.navbar.navbar-fixed-top + .navbar-inner + .container + - unless logged_in? + %a.btn.btn-navbar{ data: { toggle: "collapse", target: ".nav-collapse" } } + %span.icon-bar + %span.icon-bar + %span.icon-bar + = link_to root_path, class: "brand" do + = image_tag "turtle.svg", class: "brand-logo" + %span.hidden-phone= APPNAME + - if logged_in? + %ul.nav + %li.divider-vertical + %li.notifications.mock + = link_to "#", rel: "popover", data: { placement: 'bottom', content: "Test", title: "Messages", trigger: "manual", html: true }, id: "navbar-notifications-messages" do + %span.label.label-important.count + 3 + %i.icon-comments + %li.notifications.mock + = link_to "#", rel: "popover", data: { placement: 'bottom', content: "Test", title: "Notifications", trigger: "manual", html: true }, id: "navbar-notifications-notifications" do + %span.label.label-important.count + 2 + %i.icon-globe + - if current_user.admin? + %li.notifications.mock + = link_to "#", rel: "popover", data: { placement: 'bottom', content: "Test", title: "Reports", trigger: "manual", html: true }, id: "navbar-notifications-report" do + %span.label.label-important.count + 10 + %i.icon-warning-sign + %ul.nav.pull-right + %li#user-navbar-info + = link_to user_path(current_user), class: "navbar-profile-picture" do + = user_profile_picture current_user, size: [], thumbnail: false + = link_to user_path(current_user), class: "navbar-profile-name hidden-phone" do + = current_user + %li.dropdown + = link_to "#", data: { toggle: "dropdown" }, class: "dropdown-toggle" do + %i.icon-chevron-down + %ul.dropdown-menu + %li + = link_to :settings do + %i.icon-cog + = t(".user_settings") + %li + = link_to :logout do + %i.icon-signout + = t("logout") + - else + %ul.nav.pull-right + %li + = link_to auth_at_provider_path(provider: :facebook) do + %i.icon-facebook-sign + = t("login_with_facebook") + .nav-collapse + %ul.nav + %li.divider-vertical + %li= link_to t("how_it_works"), :how_it_works + %li= link_to t("about"), :about + %ul.nav.pull-right + %li.dropdown.pull-right + = link_to "#", data: { toggle: "dropdown" }, class: "dropdown-toggle" do + = AVAILABLE_LOCALES[I18n.locale] + %b.caret + %ul.dropdown-menu + - AVAILABLE_LOCALES.each do |code, native_name| + - if code != I18n.locale + %li + =link_to native_name, "?locale=#{code}" diff --git a/app/views/user_mailer/activation_needed_email.html.haml b/app/views/user_mailer/activation_needed_email.html.haml new file mode 100644 index 000000000..d70dece09 --- /dev/null +++ b/app/views/user_mailer/activation_needed_email.html.haml @@ -0,0 +1,16 @@ +!!! +%html + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"} + %body + %h1 + =t(".welcome", website: "minstrels.com", username: @user.email) + %p + =t(".successfully_signed_up", website: "minstrels.com") + %br + =t(".your_username", username: @user.email) + %br + %p + =t(".to_login_follow_link", url: @url) + %p + =t(".thanks_for_joining") diff --git a/app/views/user_mailer/activation_needed_email.text.erb b/app/views/user_mailer/activation_needed_email.text.erb new file mode 100644 index 000000000..e6b1ed0dd --- /dev/null +++ b/app/views/user_mailer/activation_needed_email.text.erb @@ -0,0 +1,9 @@ +<%= t(".welcome", website: "minstrels.com", username: @user.email) %> +=============================================== + +<%= t(".successfully_signed_up", website: "minstrels.com") %> +<%= t(".your_username", username: @user.email) %> + +<%= t(".to_login_follow_link", url: @url) %> + +<%= t(".thanks_for_joining") %> diff --git a/app/views/user_mailer/activation_success_email.html.haml b/app/views/user_mailer/activation_success_email.html.haml new file mode 100644 index 000000000..3bef57457 --- /dev/null +++ b/app/views/user_mailer/activation_success_email.html.haml @@ -0,0 +1,16 @@ +!!! +%html + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"} + %body + %h1 + =t(".congratulations", username: @user.email) + %p + =t(".successfully_activated", website: "minstrels.com") + %br + =t(".your_username", username: @user.email) + %br + %p + =t(".to_login_follow_link", url: @url) + %p + =t(".thanks_for_joining") diff --git a/app/views/user_mailer/activation_success_email.text.erb b/app/views/user_mailer/activation_success_email.text.erb new file mode 100644 index 000000000..06d8869cd --- /dev/null +++ b/app/views/user_mailer/activation_success_email.text.erb @@ -0,0 +1,9 @@ +<%= t(".congratulations", username: @user.email) %> +=============================================== + +<%= t(".successfully_signed_up", website: "minstrels.com") %> +<%= t(".user_name", username: @user.email) %> + +<%= t(".to_login_follow_link", url: @url) %> + +<%= t(".thanks_for_joining") %> diff --git a/app/views/user_mailer/reset_password_email.html.haml b/app/views/user_mailer/reset_password_email.html.haml new file mode 100644 index 000000000..6756f8836 --- /dev/null +++ b/app/views/user_mailer/reset_password_email.html.haml @@ -0,0 +1,13 @@ +!!! +%html + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ + %body + %h1 + =t(".hello", username: @user.email) + %p + =t(".request") + %p + =t(".new_password_follow_link", url: @url) + %p + =t(".have_a_great_day") diff --git a/app/views/user_mailer/reset_password_email.text.erb b/app/views/user_mailer/reset_password_email.text.erb new file mode 100644 index 000000000..94b0f7f23 --- /dev/null +++ b/app/views/user_mailer/reset_password_email.text.erb @@ -0,0 +1,8 @@ +<%= t(".hello", username: @user.email) %> +=============================================== + +<%= t(".request") %> + +<%= t(".new_password_follow_link", url: @url) %> + +<%= t(".have_a_great_day") %> diff --git a/app/views/user_sessions/_form.html.haml b/app/views/user_sessions/_form.html.haml new file mode 100644 index 000000000..061a9bce1 --- /dev/null +++ b/app/views/user_sessions/_form.html.haml @@ -0,0 +1,9 @@ += bootstrap_form_for @login, validate: true, url: user_sessions_path, method: :post, html: { class: "form-horizontal" } do |f| + + = f.text_field :email + = f.text_field :password + .control-group + .controls + = f.check_box :remember_me + .form-actions + = f.submit t('login'), class: "btn btn-primary" diff --git a/app/views/user_sessions/edit.html.haml b/app/views/user_sessions/edit.html.haml new file mode 100644 index 000000000..aaa6c8e41 --- /dev/null +++ b/app/views/user_sessions/edit.html.haml @@ -0,0 +1,7 @@ +.row + .span12 + %h1 Editing user_session + = render 'form' + = link_to 'Show', @user_session + | + = link_to 'Back', user_sessions_path diff --git a/app/views/user_sessions/new.html.haml b/app/views/user_sessions/new.html.haml new file mode 100644 index 000000000..4c7551fb9 --- /dev/null +++ b/app/views/user_sessions/new.html.haml @@ -0,0 +1,5 @@ +.row + = render "shared/flash_messages" + .span12 + %h1 Login + = render 'form' diff --git a/app/views/users/_calculator.html.haml b/app/views/users/_calculator.html.haml new file mode 100644 index 000000000..115b70d46 --- /dev/null +++ b/app/views/users/_calculator.html.haml @@ -0,0 +1,41 @@ +.modal.hide#fuel-cost-calculator + .modal-header + %btn.close{ type: "button", data: { dismiss: "modal" } } + × + %h3 Calcola consumi + .modal-body + = form_tag "consumption-calculator-form", class: "form-horizontal" do + .control-group + = label_tag :fuel_type, t(".fuel_type"), class: "control-label" + .controls + = select_tag :fuel_type, options_for_select(%w(diesel petrol lpg methane).map{ |f| [t(".fuel_#{f}"), f] }) + .control-group + = label_tag :vehicle_type, t(".vehicle_type"), class: "control-label" + .controls + = select_tag :vehicle_type, options_for_select(%w(compact standard high_consumption).map{ |f| [t(".vehicle_#{f}"), f] }, selected: "standard") + .control-group + = label_tag :fuel_price, t(".fuel_price"), class: "control-label" + .controls + .input-append + = text_field_tag :fuel_price, nil, class: "input-mini align-right" + %span.add-on> + = t(".100_km") + .control-group + = label_tag :fuel_price, t(".fuel_price"), class: "control-label" + .controls + .input-append + = text_field_tag :fuel_price, nil, class: "input-mini align-right" + %span.add-on> + € + = t(".per_liter") + %p.help-block + Prezzi medi al 16/07/2012: Benzina: 1,758 - Diesel: 1,649 - GPL: 0,741 - Metano: 0,972 + .control-group + .controls + %h3 + #{t(".fuel_cost_estimate")}: + %span#fuel-cost + %span € / km + .modal-footer + = link_to t("save"), "#", data: { dismiss: "modal" }, id: "save-fuel-cost", class: "btn btn-primary" + = link_to t("close"), "#", data: { dismiss: "modal" }, class: "btn" diff --git a/app/views/users/_form.html.haml b/app/views/users/_form.html.haml new file mode 100644 index 000000000..c5691b409 --- /dev/null +++ b/app/views/users/_form.html.haml @@ -0,0 +1,25 @@ += bootstrap_form_for @user, validate: true, html: { class: "form-horizontal" } do |f| + = f.error_messages + + %fieldset{ class: ("error" if @user.errors.any?) } + + = f.text_field :email, placeholder: User.human_attribute_name(:email) + = f.password_field :password, placeholder: User.human_attribute_name(:password) + + .control-group + %label.control-label + .controls + #recaptcha_image + .control-group.recaptcha{ class: ("error" if @user.errors.include?(:recaptcha)) } + .controls + %input#recaptcha_response_field{name: "recaptcha_response_field", type: "text", placeholder: t("recaptcha.enter_words")} + -if @user.errors.include?(:recaptcha) + %span.help-inline does not match captcha + = recaptcha_tags(display: { theme: "custom", lang: "it", custom_theme_widget: "recaptcha_widget" }) + + .control-group + .controls + = t('.terms') + + .form-actions + = f.submit t('.signup'), class: "btn btn-primary" diff --git a/app/views/users/_short_details.html.haml b/app/views/users/_short_details.html.haml new file mode 100644 index 000000000..38e41db63 --- /dev/null +++ b/app/views/users/_short_details.html.haml @@ -0,0 +1,57 @@ +.row + .span3 + = user_avatar user, user_path(user) + .span9 + .row-fluid + .span6 + - if defined?(message) + %p.balloon + = message + %h3{ style: "font-weight: normal;" } + = t(".personal_info") + %p + - if user.age + #{User.human_attribute_name(:age)}: + %b= user.age + %br + - if user.city + #{User.human_attribute_name(:city)}: + %b= user.city + %br + - if user.nationality + #{User.human_attribute_name(:nationality)}: + = nationality_flag(user, false) + %b= user.nationality_name + %br + - if user.languages.any? + #{User.human_attribute_name(:languages)}: + - user.ordered_languages.each do |language, level| + %b + #{language.name} + (#{Language.human_attribute_name("level_#{level}")}), + - # TODO remove comma + %br + - if user.education + #{User.human_attribute_name(:education)}: + %b= user.education + %br + - if user.occupation + #{User.human_attribute_name(:occupation)}: + %b= user.occupation + %br + + =# link_to "Read more", user_path(requesting_user), class: "btn" + .span6 + %h3{ style: "font-weight: normal;" } + = t(".about") + %p.align-justify + = user.short_about + - if user.references.visible.filled_up.any? + %h3{ style: "font-weight: normal;" } + = t(".references") + (#{user.references.visible.filled_up.size}) + = references_summary_tags(user) + %p + - user.references.visible.filled_up.sample(2).each do |reference| + = reference_snippet(reference) + %br diff --git a/app/views/users/banned.html.haml b/app/views/users/banned.html.haml new file mode 100644 index 000000000..1c1fd5220 --- /dev/null +++ b/app/views/users/banned.html.haml @@ -0,0 +1,8 @@ +- title t(".title") +.row + .span12 + .hero-unit + %h1.pacifico= t(".you_were_banned") + .spacer + %p + Contact our staff if you think there was a mistake. diff --git a/app/views/users/dashboard.html.haml b/app/views/users/dashboard.html.haml new file mode 100644 index 000000000..8d3e72313 --- /dev/null +++ b/app/views/users/dashboard.html.haml @@ -0,0 +1,32 @@ +- title t(".title") +.row + .span6 + #hero-user.hero-unit + .attribution + db9_dash.jpg by xmatt, on Flickr + .pacifico.dashboard-title= t(".reminder") + %p + = link_to :conversations, class: "btn btn-large btn-block disabled" do + %i.icon-comments.icon-large + = t(".new_messages", count: 0) + %p + = link_to :notifications, class: "btn btn-large btn-block" do + %i.icon-globe.icon-large + 2 notifiche + .span6 + #hero-itineraries.hero-unit + .attribution + _IGP5461 | 70 by Ben Fredericson (xjrlokix), on Flickr + .pacifico.dashboard-title= Itinerary.model_name.human(count: 42) + %p + = link_to itineraries_path, class: "btn btn-large btn-block" do + %i.icon-search.icon-large + = t(".search_itineraries") + %p + = link_to new_itinerary_path, class: "btn btn-large btn-block " do + %i.icon-road.icon-large + = t(".new_itinerary") + %p + = link_to my_itineraries_path, class: "btn btn-large btn-block " do + %i.icon-heart.icon-large + = t(".your_itineraries") diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100644 index 000000000..f56a858f1 --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,37 @@ += title User.model_name.human(count: :lots) +.row + .span12 + = render "shared/flash_messages" + %h1= yield :title + .span12 + %table.table.table-striped + %thead + %th ID + %th Name + %th Email + %th Actions + %tbody + - @users.sorted.each do |user, index| + %tr.vertical-align-middle + %td= user.id + %td + = link_to user, user_path(user) + %td= user.email + %td + = link_to edit_user_path(user), class: "btn btn-small" do + %i.icon-edit + = t("helpers.links.edit") +   + - if user.banned? + = link_to unban_user_path(user), method: :post, data: { confirm: t("helpers.links.confirm") }, class: "btn btn-small btn-warning" do + = t("helpers.links.user.unban") + - else + = link_to ban_user_path(user), method: :post, data: { confirm: t("helpers.links.confirm") }, class: "btn btn-small btn-warning" do + %i.icon-ban-circle + = t("helpers.links.user.ban") +   + = link_to user_path(user), method: :delete, data: { confirm: t("helpers.links.confirm") }, class: "btn btn-small btn-danger" do + %i.icon-trash + = t("helpers.links.destroy") + + = paginate @users diff --git a/app/views/users/settings.html.haml b/app/views/users/settings.html.haml new file mode 100644 index 000000000..66025ba14 --- /dev/null +++ b/app/views/users/settings.html.haml @@ -0,0 +1,45 @@ +- title t(".user_settings") +.row + .span12 + = render "shared/flash_messages" + %h1= yield(:title) +.row + .span12 + = bootstrap_form_for @user, validate: true, html: { class: "form-horizontal" } do |f| + = f.error_messages + %fieldset + .control-group + .controls + %p.help-block + %i.icon-info-sign.icon-large + = t(".user_profile_details") + = f.select :nationality, Country.sorted.collect {|c| [ c._name, c.code ] }, { include_blank: true }, help: false + %legend= t(".vehicle_information") + %fieldset + .control-group{ class: ("error" if @user.errors.include?(:vehicle_avg_consumption)) } + = f.label :vehicle_avg_consumption, class: "control-label" + .controls + .input-append + = f.default_tag :text_field, :vehicle_avg_consumption, class: "input-mini align-right" + %span.add-on> + € / km + - if @user.errors.include?(:vehicle_avg_consumption) + %span.help-inline + = @user.errors.messages[:vehicle_avg_consumption].join(", ") + %p.help-block + = t(".vehicle_avg_consumption_help") + = link_to t(".km_costs_calculation_link"), "http://servizi.aci.it/CKInternet/", target: "_blank" + %br + %small + = t(".vehicle_avg_consumption_note", fuel: content_tag(:u, t(".fuel")), proportional_costs: content_tag(:u, t(".proportional_costs"))).html_safe + %legend= t(".application_settings") + %fieldset + = f.select :locale, options_for_select(AVAILABLE_LOCALES.map { |code, native_name| [ native_name, code] }, @user.locale), {}, disabled: (AVAILABLE_LOCALES.size == 1) + = f.time_zone_select :time_zone + .control-group + = label_tag :notification_group, t(".send_email"), class: "control-label" + .controls + = f.check_box :send_email_messages + = f.check_box :send_email_references + .form-actions + = f.submit class: "btn btn-primary" diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml new file mode 100644 index 000000000..594c2f692 --- /dev/null +++ b/app/views/users/show.html.haml @@ -0,0 +1,75 @@ +- title @user +.row + .span3 + = user_profile_picture @user, size: [], type: :large, class: "border-box" + .span9 + %h1.pacifico + = @user + - if @user.bio? + %blockquote + %p= @user.bio + %dl.dl-horizontal.tag-list + %dt + %span Referenze + %dd + %span>< + 0 positive + %span>< + 0 neutre + %span>< + 0 negative + = link_to "leggi tutte", "#" + %dt + %span.description-facebook Informazioni di base + %dd + %span>< + #{@facebook_details[:friends]} amici + %span{ class: ("common" if @user != current_user && @user.age == current_user.age) }>< + #{@user.age} anni + - if @user.nationality? + %span{ class: ("common" if @user != current_user && @user.nationality == current_user.nationality) }>< + = @user.nationality_name + %span{ class: ("common" if @user != current_user && @user.gender == current_user.gender) }>< + #{User.human_attribute_name("gender_#{@user.gender}")} + - if @user.languages? + - common_languages = @user.languages.map{ |lang| lang["id"] } & current_user.languages.map{ |lang| lang["id"] } + - @user.languages.each do |language| + %span{ class: ("common" if @user != current_user && common_languages.include?(language["id"])) }>< + = t(".language", language: language["name"]) + - if current_user != @user + - if (mutual_friends = @facebook_details[:mutualfriends] || {}).any? + %dt + %span.description-facebook Amici in comune + %dd.friends + - mutual_friends.sample(5).each do |friend| + %span>< + = user_profile_picture friend["id"], size: [25,25], thumbnail: false + = friend["name"] + - if mutual_friends.size - 5 > 0 + = link_to "e altri #{mutual_friends.size - 5}", "#" + - if @user.education? && @user.education.any? + %dt + %span.description-facebook Educazione + %dd + - common_education = @user.education.map{ |edu| edu.first.second["id"] } & current_user.education.map{ |edu| edu.first.second["id"] } + - @user.education.each do |element| + %span{ class: ("common" if @user != current_user && common_education.include?(element.first.second["id"])) }>< + = element.first.second["name"] + - if @user.work? && @user.work.any? + %dt + %span.description-facebook Lavoro + %dd + - common_work = @user.work.map{ |w| w.first.second["id"] } & current_user.work.map{ |w| w.first.second["id"] } + - @user.work.each do |work| + %span{ class: ("common" if @user != current_user && common_work.include?(work.first.second["id"])) }>< + = work.first.second["name"] + %dt + %span.description-facebook Mi piace + %dd + - user_likes = @facebook_details[:likes] + - if @user != current_user + - my_likes = current_user.facebook_likes + - common_likes = user_likes.map{ |like| like["id"] } & my_likes.map{ |like| like["id"] } + - user_likes.each do |like| + %span{ class: ("common" if @user != current_user && common_likes.include?(like["id"])) }>< + = like["name"] diff --git a/app/workers/email_sender.rb b/app/workers/email_sender.rb new file mode 100644 index 000000000..dada6a550 --- /dev/null +++ b/app/workers/email_sender.rb @@ -0,0 +1,12 @@ +class EmailSender + @queue = :emails_queue + + def self.perform(mailer, method, user_id) + user = User.find(user_id) + mailer_class = Kernel.const_get(mailer) + mail = mailer_class.send(method.to_sym, user) + if defined?(ActionMailer) and mailer_class.superclass == ActionMailer::Base + mail.deliver + end + end +end diff --git a/app/workers/facebook_timeline_updater.rb b/app/workers/facebook_timeline_updater.rb new file mode 100644 index 000000000..9183745f9 --- /dev/null +++ b/app/workers/facebook_timeline_updater.rb @@ -0,0 +1,14 @@ +class FacebookTimelineUpdater + @queue = :facebook_timeline_queue + def self.perform(itinerary_id) + itinerary = Itinerary.find(itinerary_id) + itinerary_url = Rails.application.routes.url_helpers.itinerary_url(itinerary, host: APP_CONFIG.base_url) + user = itinerary.user + user.facebook do |fb| + permissions = fb.get_connections("me", "permissions") + if permissions[0]["publish_stream"].to_i == 1 + fb.put_connections("me", "codenameicare:plan", itinerary: itinerary_url) + end + end + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 000000000..f4f3831ed --- /dev/null +++ b/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Icare::Application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 000000000..3d69de919 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,75 @@ +require File.expand_path('../boot', __FILE__) + +require "action_controller/railtie" +require "action_mailer/railtie" +require "active_resource/railtie" +require "rails/test_unit/railtie" +require "sprockets/railtie" + +if defined?(Bundler) + # If you precompile assets before deploying to production, use this line + Bundler.require(*Rails.groups(assets: %w(development test))) + # If you want your assets lazily compiled in production, use this line + # Bundler.require(:default, :assets, Rails.env) +end + +module Icare + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Custom directories with classes and modules you want to be autoloadable. + config.autoload_paths += %W(#{config.root}/app/uploaders) + config.autoload_paths += %W(#{config.root}/lib/*) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + config.mongoid.observers = :itinerary_observer + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] + config.i18n.default_locale = "en-US" + config.i18n.available_locales = %w(en-US it-IT) + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + + # Use SQL instead of Active Record's schema dumper when creating the database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Enforce whitelist mode for mass assignment. + # This will create an empty whitelist of attributes available for mass-assignment for all models + # in your app. As such, your models will need to explicitly whitelist or blacklist accessible + # parameters by using an attr_accessible or attr_protected declaration. + # config.active_record.whitelist_attributes = true + + # Enable the asset pipeline + config.assets.enabled = true + config.assets.initialize_on_precompile = false + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + + config.generators do |g| + g.template_engine :haml + end + + # Logging with Unicorn on Heroku + #config.logger = Logger.new(STDOUT) + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 000000000..4489e5868 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,6 @@ +require 'rubygems' + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff --git a/config/configuration.rb b/config/configuration.rb new file mode 100644 index 000000000..7c68bb4e1 --- /dev/null +++ b/config/configuration.rb @@ -0,0 +1,19 @@ +SimpleConfig.for :application do + + # Set here your global configuration. + # All settings can be overwritten later per-environment. + load File.join(Rails.root.to_s, "config", "settings", "application.rb"), :if_exists? => true + + # Per Environment settings. + # At startup only the file matching current environment will be loaded. + # Settings stored here will overwrite settings with the same name stored in application.rb + load File.join(Rails.root.to_s, "config", "settings", "#{Rails.env}.rb"), :if_exists? => true + + # Local settings. It is designed as a place for you to override variables + # specific to your own development environment. + # Make sure your version control system ignores this file otherwise + # you risk checking in a file that will override values in production + load File.join(Rails.root.to_s, "config", "settings", "local.rb"), :if_exists? => true +end + +APP_CONFIG = SimpleConfig.for :application diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 000000000..b1605598f --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: --drb <%= std_opts %> features +wip: --drb --tags @wip:3 --wip features +rerun: --drb <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 000000000..d229f23b3 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,10 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +require File.join(Rails.root.to_s, "config", "configuration.rb") + +# Initialize the rails application +Icare::Application.initialize! + +# Enable the Haml ugly option +Haml::Template.options[:ugly] = true diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 000000000..651f8bef1 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,37 @@ +Icare::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send + config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger + config.active_support.deprecation = :log + + # Only use best-standards-support built into browsers + config.action_dispatch.best_standards_support = :builtin + + # Raise exception on mass assignment protection for Active Record models + # config.active_record.mass_assignment_sanitizer = :strict + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 + + # Do not compress assets + config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = true +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 000000000..2d02a4989 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,86 @@ +Icare::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # Enable deflater + config.middleware.use Rack::Deflater + + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false + + # Compress JavaScripts and CSS + config.assets.compress = true + + # Don't fallback to assets pipeline if a precompiled asset is missed + config.assets.compile = true + + # Generate digests for assets URLs + config.assets.digest = true + + # Defaults to Rails.root.join("public/assets") + # config.assets.manifest = YOUR_PATH + + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # See everything in the log (default is :info) + # config.log_level = :debug + + # Prepend all log lines with the following tags + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # config.assets.precompile += %w( search.js ) + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + + # Enable threaded mode + # config.threadsafe! + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 + + config.action_mailer.default_url_options = { host: APP_CONFIG.mailer.host } + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = false + config.action_mailer.default charset: "utf-8" + config.action_mailer.default from: APP_CONFIG.mailer.from + config.action_mailer.smtp_settings = { + address: APP_CONFIG.mailer.address, + port: APP_CONFIG.mailer.port, + domain: APP_CONFIG.mailer.domain, + authentication: "plain", + enable_starttls_auto: true, + user_name: APP_CONFIG.mailer.user_name, + password: APP_CONFIG.mailer.password + } +end diff --git a/config/environments/staging.rb b/config/environments/staging.rb new file mode 100644 index 000000000..2d02a4989 --- /dev/null +++ b/config/environments/staging.rb @@ -0,0 +1,86 @@ +Icare::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # Enable deflater + config.middleware.use Rack::Deflater + + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false + + # Compress JavaScripts and CSS + config.assets.compress = true + + # Don't fallback to assets pipeline if a precompiled asset is missed + config.assets.compile = true + + # Generate digests for assets URLs + config.assets.digest = true + + # Defaults to Rails.root.join("public/assets") + # config.assets.manifest = YOUR_PATH + + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # See everything in the log (default is :info) + # config.log_level = :debug + + # Prepend all log lines with the following tags + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # config.assets.precompile += %w( search.js ) + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + + # Enable threaded mode + # config.threadsafe! + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 + + config.action_mailer.default_url_options = { host: APP_CONFIG.mailer.host } + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = false + config.action_mailer.default charset: "utf-8" + config.action_mailer.default from: APP_CONFIG.mailer.from + config.action_mailer.smtp_settings = { + address: APP_CONFIG.mailer.address, + port: APP_CONFIG.mailer.port, + domain: APP_CONFIG.mailer.domain, + authentication: "plain", + enable_starttls_auto: true, + user_name: APP_CONFIG.mailer.user_name, + password: APP_CONFIG.mailer.password + } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 000000000..975c4f6db --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,37 @@ +Icare::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Configure static asset server for tests with Cache-Control for performance + config.serve_static_assets = true + config.static_cache_control = "public, max-age=3600" + + # Log error messages when you accidentally call methods on nil + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :cache + + # Raise exception on mass assignment protection for Active Record models + # config.active_record.mass_assignment_sanitizer = :strict + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr +end diff --git a/config/exceptional.yml b/config/exceptional.yml new file mode 100644 index 000000000..f4ad5ac32 --- /dev/null +++ b/config/exceptional.yml @@ -0,0 +1,12 @@ +# Set an ENV variable EXCEPTIONAL_API_KEY +development: + enabled: false + +test: + enabled: false + +staging: + enabled: true + +production: + enabled: true diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 000000000..59385cdf3 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb new file mode 100644 index 000000000..4d372bdc5 --- /dev/null +++ b/config/initializers/bullet.rb @@ -0,0 +1,8 @@ +if defined? Bullet + Bullet.enable = true + Bullet.alert = true + Bullet.bullet_logger = true + Bullet.console = true + Bullet.rails_logger = true + Bullet.disable_browser_cache = true +end diff --git a/config/initializers/client_side_validations.rb b/config/initializers/client_side_validations.rb new file mode 100644 index 000000000..c15fe4729 --- /dev/null +++ b/config/initializers/client_side_validations.rb @@ -0,0 +1,14 @@ +# ClientSideValidations Initializer + +require 'client_side_validations/simple_form' if defined?(::SimpleForm) +require 'client_side_validations/formtastic' if defined?(::Formtastic) + +# Uncomment the following block if you want each input field to have the validation messages attached. +ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| + #unless html_tag =~ /^