diff --git a/.env.sample b/.env.sample index fbd9811..e42052b 100644 --- a/.env.sample +++ b/.env.sample @@ -14,4 +14,4 @@ TWILIO_SID=none TWILIO_AUTH_TOKEN=none TWILIO_SENDER_PHONE_NUMBER=none SELF_HOSTED=true -LINK_TO_GITHUB_REPO=https://github.com/garrettqmartin8/volition +LINK_TO_GITHUB_REPO=https://github.com/usevolition/volition diff --git a/.erdconfig b/.erdconfig new file mode 100644 index 0000000..10543ad --- /dev/null +++ b/.erdconfig @@ -0,0 +1,4 @@ +attributes: + - content + - foreign_key + - inheritance diff --git a/.torus.json b/.torus.json new file mode 100644 index 0000000..5db27df --- /dev/null +++ b/.torus.json @@ -0,0 +1 @@ +{"org":"garrettqmartin","project":"volition"} diff --git a/CONDUCT.md b/CONDUCT.md new file mode 100644 index 0000000..7f72b35 --- /dev/null +++ b/CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org diff --git a/Gemfile b/Gemfile index 60782e9..a045a26 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem "jquery-rails" gem "appsignal" gem "sidekiq" gem "normalize-rails", "~> 3.0.0" -gem "pg" +gem "pg", "0.21.0" gem 'turbolinks' gem "puma" gem "rails", "~> 5.1.0" @@ -23,7 +23,7 @@ gem "jbuilder" gem "listen" gem "spring" gem 'letter_opener' -gem 'react-rails' +gem 'react-rails', "2.2.0" gem 'bcrypt' gem 'will_paginate', '~> 3.1.0' gem 'twilio-ruby' @@ -40,6 +40,8 @@ gem 'google-id-token', git: 'https://github.com/google/google-id-token.git' gem 'enumerize' gem "gibbon" gem 'premailer-rails' +gem 'rails-erd' +gem "payola-payments", git: "https://github.com/payolapayments/payola.git", branch: "master" group :development, :test do gem "awesome_print" diff --git a/Gemfile.lock b/Gemfile.lock index c3f30e8..66ffda0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,72 +1,84 @@ GIT remote: https://github.com/google/google-id-token.git - revision: 4d0f02d70f6d5bef4f6759ccf18bf4e76bc60803 + revision: 858acae389fdaf75f0c0ab3199487b22c29687e6 specs: - google-id-token (1.3.1) + google-id-token (1.4.2) jwt (>= 1) - multi_json + +GIT + remote: https://github.com/payolapayments/payola.git + revision: 08bd7bc31aa3be431e36ea73e594613a7042816f + branch: master + specs: + payola-payments (1.5.1) + aasm (>= 4.0.7) + jquery-rails + rails (>= 4.1) + stripe (>= 2.8) + stripe_event (>= 2.0.0) GIT remote: https://github.com/rebelidealist/stripe-ruby-mock.git - revision: 310fdab94df322a36d4ed2764b3c49356e253829 + revision: 665f2088a71ed810e20ef362fbd435ce5b78fc6b branch: master specs: - stripe-ruby-mock (2.4.1) + stripe-ruby-mock (2.5.1) dante (>= 0.2.0) multi_json (~> 1.0) - stripe (>= 1.31.0, <= 1.58.0) + stripe (>= 2.0.3) GEM remote: https://rubygems.org/ specs: - actioncable (5.1.1) - actionpack (= 5.1.1) + aasm (4.12.3) + concurrent-ruby (~> 1.0) + actioncable (5.1.4) + actionpack (= 5.1.4) nio4r (~> 2.0) websocket-driver (~> 0.6.1) - actionmailer (5.1.1) - actionpack (= 5.1.1) - actionview (= 5.1.1) - activejob (= 5.1.1) + actionmailer (5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.1) - actionview (= 5.1.1) - activesupport (= 5.1.1) + actionpack (5.1.4) + actionview (= 5.1.4) + activesupport (= 5.1.4) rack (~> 2.0) - rack-test (~> 0.6.3) + rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.1) - activesupport (= 5.1.1) + actionview (5.1.4) + activesupport (= 5.1.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.1.1) - activesupport (= 5.1.1) + activejob (5.1.4) + activesupport (= 5.1.4) globalid (>= 0.3.6) - activemodel (5.1.1) - activesupport (= 5.1.1) - activerecord (5.1.1) - activemodel (= 5.1.1) - activesupport (= 5.1.1) + activemodel (5.1.4) + activesupport (= 5.1.4) + activerecord (5.1.4) + activemodel (= 5.1.4) + activesupport (= 5.1.4) arel (~> 8.0) - activesupport (5.1.1) + activesupport (5.1.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) ansi (1.5.0) - appsignal (2.2.1) + appsignal (2.4.3) rack - thread_safe arel (8.0.0) - ast (2.3.0) - autoprefixer-rails (7.1.1) + ast (2.4.0) + autoprefixer-rails (7.2.5) execjs - awesome_print (1.7.0) + awesome_print (1.8.0) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -77,24 +89,25 @@ GEM execjs (~> 2.0) bcrypt (3.1.11) builder (3.2.3) - bullet (5.5.1) + bullet (5.7.2) activesupport (>= 3.0.0) - uniform_notifier (~> 1.10.0) - bundler-audit (0.5.0) + uniform_notifier (~> 1.11.0) + bundler-audit (0.6.0) bundler (~> 1.2) thor (~> 0.18) - byebug (9.0.6) - capybara (2.14.0) + byebug (9.1.0) + capybara (2.17.0) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - xpath (~> 2.0) + xpath (>= 2.0, < 4.0) + choice (0.2.0) climate_control (0.2.0) - codeclimate-engine-rb (0.4.0) + codeclimate-engine-rb (0.4.1) virtus (~> 1.0) - coderay (1.1.1) + coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) concurrent-ruby (1.0.5) @@ -102,29 +115,27 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.3) - css_parser (1.5.0) + css_parser (1.6.0) addressable dante (0.2.0) - database_cleaner (1.6.1) + database_cleaner (1.6.2) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) docile (1.1.5) - domain_name (0.5.20170404) - unf (>= 0.0.5, < 1.0.0) dotenv (2.2.1) dotenv-rails (2.2.1) dotenv (= 2.2.1) railties (>= 3.2, < 5.2) - enumerize (2.1.0) + enumerize (2.1.2) activesupport (>= 3.2) equalizer (0.0.11) erubi (1.7.0) erubis (2.7.0) execjs (2.7.0) - faraday (0.13.1) + faraday (0.14.0) multipart-post (>= 1.2, < 3) ffi (1.9.18) - flay (2.9.0) + flay (2.10.0) erubis (~> 2.7.0) path_expander (~> 1.0) ruby_parser (~> 3.0) @@ -134,31 +145,28 @@ GEM ruby_parser (~> 3.1, > 3.1.0) sexp_processor (~> 4.8) flutie (2.0.0) - font-awesome-rails (4.7.0.2) + font-awesome-rails (4.7.0.3) railties (>= 3.2, < 5.2) formulaic (0.4.0) activesupport capybara i18n - gibbon (3.1.0) + gibbon (3.2.0) faraday (>= 0.9.1) multi_json (>= 1.11.0) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) - gon (6.1.0) + gon (6.2.0) actionpack (>= 3.0) - json multi_json request_store (>= 1.0) - google_sign_in (0.1) + google_sign_in (0.1.4) activesupport (>= 5.1) - google-id-token (>= 1.3.1) - hashdiff (0.3.4) + google-id-token (>= 1.4.0) + hashdiff (0.3.7) high_voltage (3.0.0) htmlentities (4.3.4) - http-cookie (1.0.3) - domain_name (~> 0.5) - i18n (0.9.1) + i18n (0.9.3) concurrent-ruby (~> 1.0) ice_nine (0.11.2) jbuilder (2.7.0) @@ -169,10 +177,10 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.1.0) - jwt (1.5.6) + jwt (2.1.0) launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.4.1) + letter_opener (1.6.0) launchy (~> 2.2) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) @@ -181,73 +189,74 @@ GEM loofah (2.1.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.6.5) - mime-types (>= 1.16, < 4) - method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mail (2.7.0) + mini_mime (>= 0.1.1) + method_source (0.9.0) + mini_mime (1.0.0) mini_portile2 (2.3.0) - minitest (5.10.3) - minitest-reporters (1.1.14) + minitest (5.11.3) + minitest-reporters (1.1.19) ansi builder minitest (>= 5.0) ruby-progressbar - minitest-stub_any_instance (1.0.1) - multi_json (1.12.1) + minitest-stub_any_instance (1.0.2) + multi_json (1.13.1) multipart-post (2.0.0) - netrc (0.11.0) - nio4r (2.1.0) - nokogiri (1.8.1) + nio4r (2.2.0) + nokogiri (1.8.2) mini_portile2 (~> 2.3.0) normalize-rails (3.0.3) - parser (2.4.0.0) - ast (~> 2.2) + parser (2.4.0.2) + ast (~> 2.3) path_expander (1.0.2) - pg (0.20.0) - premailer (1.10.4) + pg (0.21.0) + premailer (1.11.1) addressable - css_parser (>= 1.4.10) + css_parser (>= 1.6.0) htmlentities (>= 4.0.0) - premailer-rails (1.9.6) + premailer-rails (1.10.1) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.5.1) + byebug (~> 9.1) pry (~> 0.10) pry-rails (0.3.6) pry (>= 0.10.4) - public_suffix (2.0.5) - puma (3.9.1) - rack (2.0.3) - rack-cors (0.4.1) - rack-mini-profiler (0.10.5) + public_suffix (3.0.1) + puma (3.11.2) + rack (2.0.4) + rack-cors (1.0.2) + rack-mini-profiler (0.10.7) rack (>= 1.2.0) rack-protection (2.0.0) rack - rack-test (0.6.3) - rack (>= 1.0) + rack-test (0.8.2) + rack (>= 1.0, < 3) rack-timeout (0.4.2) - rails (5.1.1) - actioncable (= 5.1.1) - actionmailer (= 5.1.1) - actionpack (= 5.1.1) - actionview (= 5.1.1) - activejob (= 5.1.1) - activemodel (= 5.1.1) - activerecord (= 5.1.1) - activesupport (= 5.1.1) - bundler (>= 1.3.0, < 2.0) - railties (= 5.1.1) + rails (5.1.4) + actioncable (= 5.1.4) + actionmailer (= 5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) + activemodel (= 5.1.4) + activerecord (= 5.1.4) + activesupport (= 5.1.4) + bundler (>= 1.3.0) + railties (= 5.1.4) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) + rails-erd (1.5.2) + activerecord (>= 3.2) + activesupport (>= 3.2) + choice (~> 0.2.0) + ruby-graphviz (~> 1.2) rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails_12factor (0.0.3) @@ -255,18 +264,18 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.1.1) - actionpack (= 5.1.1) - activesupport (= 5.1.1) + railties (5.1.4) + actionpack (= 5.1.4) + activesupport (= 5.1.4) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - rake (12.0.0) - rb-fsevent (0.9.8) - rb-inotify (0.9.8) - ffi (>= 0.5.0) + rake (12.3.0) + rb-fsevent (0.10.2) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) react-rails (2.2.0) babel-transpiler (>= 0.7.0) connection_pool @@ -275,7 +284,7 @@ GEM tilt recipient_interceptor (0.1.2) mail - redis (3.3.5) + redis (4.0.1) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) redis-rack (>= 1, < 3) @@ -292,20 +301,18 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) - reek (4.7.0) + reek (4.7.3) codeclimate-engine-rb (~> 0.4.0) parser (>= 2.4.0.0, < 2.5) rainbow (~> 2.0) - request_store (1.3.2) - rest-client (2.0.2) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - ruby-progressbar (1.8.1) + request_store (1.4.0) + rack (>= 1.4) + ruby-graphviz (1.2.3) + ruby-progressbar (1.9.0) ruby_dep (1.5.0) - ruby_parser (3.9.0) - sexp_processor (~> 4.1) - rubycritic (3.2.3) + ruby_parser (3.10.1) + sexp_processor (~> 4.9) + rubycritic (3.3.0) flay (~> 2.8) flog (~> 4.4) launchy (= 2.4.3) @@ -313,29 +320,33 @@ GEM rainbow (~> 2.1) reek (~> 4.4) ruby_parser (~> 3.8) + tty-which (~> 0.3.0) virtus (~> 1.0) safe_yaml (1.0.4) - sass (3.4.24) - sass-rails (5.0.6) + sass (3.5.5) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sass-rails (5.0.7) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sexp_processor (4.9.0) - shoulda-matchers (3.1.1) + sexp_processor (4.10.0) + shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.0.2) + sidekiq (5.0.5) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) - simplecov (0.14.1) + redis (>= 3.3.4, < 5) + simplecov (0.15.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) - simplecov-html (0.10.1) - slop (3.6.0) + simplecov-html (0.10.2) spring (2.0.2) activesupport (>= 4.2) sprockets (3.7.1) @@ -345,52 +356,50 @@ GEM babel-source (>= 5.8.11) babel-transpiler sprockets (>= 3.0.0) - sprockets-rails (3.2.0) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - stripe (1.58.0) - rest-client (>= 1.4, < 4.0) - stripe_event (1.6.0) + stripe (3.9.1) + faraday (~> 0.10) + stripe_event (2.1.1) activesupport (>= 3.1) - stripe (>= 1.6, < 3.0) - thor (0.19.4) + stripe (>= 2.8, < 4.0) + thor (0.20.0) thread_safe (0.3.6) - tilt (2.0.7) - timecop (0.8.1) + tilt (2.0.8) + timecop (0.9.1) title (0.0.7) i18n rails (>= 3.1) - turbolinks (5.0.1) - turbolinks-source (~> 5) - turbolinks-source (5.0.3) - twilio-ruby (4.13.0) - builder (>= 2.1.2) - jwt (~> 1.0) - multi_json (>= 1.3.0) + tty-which (0.3.0) + turbolinks (5.1.0) + turbolinks-source (~> 5.1) + turbolinks-source (5.1.0) + twilio-ruby (5.6.2) + faraday (~> 0.9) + jwt (>= 1.5, <= 2.5) + nokogiri (>= 1.6, < 2.0) tzinfo (1.2.4) thread_safe (~> 0.1) - uglifier (3.2.0) + uglifier (4.1.5) execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.4) - uniform_notifier (1.10.0) + uniform_notifier (1.11.0) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - webmock (3.0.1) + webmock (3.3.0) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) - will_paginate (3.1.5) - xpath (2.1.0) - nokogiri (~> 1.3) + websocket-extensions (0.1.3) + will_paginate (3.1.6) + xpath (3.0.0) + nokogiri (~> 1.8) PLATFORMS ruby @@ -422,7 +431,8 @@ DEPENDENCIES minitest-reporters minitest-stub_any_instance normalize-rails (~> 3.0.0) - pg + payola-payments! + pg (= 0.21.0) premailer-rails pry-byebug pry-rails @@ -431,9 +441,10 @@ DEPENDENCIES rack-mini-profiler rack-timeout rails (~> 5.1.0) + rails-erd rails_12factor rails_stdout_logging - react-rails + react-rails (= 2.2.0) recipient_interceptor redis-rails rubycritic diff --git a/LICENSE b/LICENSE.md similarity index 52% rename from LICENSE rename to LICENSE.md index 35d255b..549f03e 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,7 +1,9 @@ -Copyright 2017 Garrett Martin +# MIT License + +Copyright © 2018 Volition Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The Software is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. diff --git a/README.md b/README.md index 1206b3c..ef0b011 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,18 @@ -# Volition +Volition -Build status: [![CircleCI](https://circleci.com/gh/garrettqmartin8/volition.svg?style=svg&circle-token=f883f7406ee9df386967c67b4a6f5a330083fe29)](https://circleci.com/gh/garrettqmartin8/volition) +### The power of using one's will. -## Self hosting HEROKU -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/garrettqmartin8/volition) +[![CircleCI](https://circleci.com/gh/usevolition/volition/tree/master.svg?style=shield)](https://circleci.com/gh/usevolition/volition/tree/master) -Volition is designed with self hosting in mind. By using “Deploy to Heroku” button above, you can be up and running in a few minutes! +--- -To update your deployed app with the lastest changes: +Volition is more than a todo list. It’s a framework for becoming more effective. It encourages you to be intentional in planning your day and reflecting on the decisions you've made. Read about [the philosophy behind Volition](https://usevolition.com/philosophy "Philosophy behind Volition") to learn more. -1. Clone this repo: `git clone git@github.com:garrettqmartin8/volition.git` -2. Run `bin/update_heroku` from the command line. -3. Enjoy your updated app! +## Self-Hosting Volition +#### We believe users should control their own data +New web applications and software products seem to be more ephemeral than ever. This is the reason Volition's users have the option to self-host our software. If Volition helps you as much as it has helped us, we want you to rest easy knowing you'll have it forever. Refer to our [self-hosting guidelines](SELFHOST.md "Volition self hosting guidelines") for more details on deploying and keeping your application updated. -## Self hosting Ubuntu - -Volition runs great on Ubuntu Server. Tested on Ubuntu 14.04 (YMMV) - -### Prerequisites for Ubuntu install: - -1. Ruby 2.4.0 (install using rvm) -- _make sure rvm installed:_ -- `sudo apt-get install libgdbm-dev libncurses5-dev automake libtool bison libffi-dev` -- `gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB` -- `\curl -sSL https://get.rvm.io | bash -s stable` -- `source ~/.rvm/scripts/rvm` -- _then install pre-stuff for ruby:_ -- `sudo apt-add-repository ppa:brightbox/ruby-ng` -- `sudo apt-get update` -- `sudo apt-get install libpq-dev git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev python-software-properties libffi-dev nodejs` -- `rvm install 2.4.0` -- `rvm use 2.4.0 --default` -- `ruby -v`_should be 2.4.0_ - -2. PostgreSQL -- `sudo apt-get update` -- `sudo apt-get install postgresql postgresql-contrib` -- edit the postgres config to trust local user connections by default -- `sudo nano /etc/postgresql/9.3/main/pg_hba.conf` -- edit the lines for local to be trust instead of peer or md5: -``` -# Database administrative login by Unix domain socket -local all postgres trust - -# TYPE DATABASE USER ADDRESS METHOD - -# "local" is for Unix domain socket connections only -local all all trust -``` -### Post setup of Ruby & Postgres on Ubuntu Server - -You can try just running ./bin/setup at this point as mentioned at the bottom of this readme, but I found I had to work through the following three gotchas before anything worked. - -1. Edit config/database.yml and add in `username: postgres` and `password:` fields for development and production sections -2. install bundler gem: `gem install bundler` -3. config bundler to get `pg` to install properly: `bundle config build.pg --with-pg-lib="/var/lib/postgresql/9.3/main"` - -### Run on local port 3000 on Ubuntu - -Simply `rails -s` - -You can run this in `tmux` or daemonize this process using `passenger` or `systemd` - -### setup with NGINX reverse proxy - -create a new NGINX vhost file in /etc/nginx/sites-available/, for example /etc/nginx/sites-available/volition - -here is example volition file: - -``` -server { - server_name do.jamescampbell.us; - listen 80; - - access_log /var/log/nginx/do-access.log; - error_log /var/log/nginx/do-error.log; - - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_cache_bypass $http_upgrade; - proxy_pass http://127.0.0.1:3000; - } - -} -``` - -and go to what you set in your server\_name field and it should just work - - -### Setting up reminders -To take advantage of the daily reflection reminders, you may need to do some extra setup: - -#### SMS reminders -You’ll need to create a Twilio account and set three environment variables in the `.env` file: -- `TWILIO_SID` -- `TWILIO_AUTH_TOKEN` -- `TWILIO_SENDER_PHONE_NUMBER` - -#### Email reminders -You’ll need to set some SMTP environment variables in the `.env` file: -- `SMTP_ADDRESS` -- `SMTP_DOMAIN` -- `SMTP_PASSWORD` -- `SMTP_USERNAME` - -## Contributing - -After you have cloned this repo, run this setup script to set up your machine -with the necessary dependencies to run and test this app: - - % ./bin/setup - -It assumes you have a machine equipped with Ruby, Postgres, etc. If not, set up -your machine with [this script]. - -[this script]: https://github.com/thoughtbot/laptop - -PRs are welcome! - -MIT License +## Contributing to Volition +Volition will always be [MIT-licened](LICENSE.md "Volition MIT-License") open source software. We welcome all contributions. If you find a bug, feel free to fork Volition and submit a PR. We expect all Volition contributors to abide by the terms of our [code of conduct](CONDUCT.md "Volition code of conduct"). +#### © 2018 Volition diff --git a/SELFHOST.md b/SELFHOST.md new file mode 100644 index 0000000..cb951d9 --- /dev/null +++ b/SELFHOST.md @@ -0,0 +1,19 @@ +# Self-Hosting Volition + +Volition is designed with self hosting in mind. Follow the steps below and you'll be up and running in no time. + +## Deploying to Heroku + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/usevolition/volition) + +1. command + click the button above to open your "Create New App" Heroku dashboard in a new tab. + +2. Name your Heroku application whatever you want in the "App name" input box at the top of the "Create New App" form. + +3. Click "Deploy app" at the bottom of the page. Heroku will provide a checklist of steps that it needs to complete. + +4. Once Heroku has completed all the steps necessary for deployment, you'll have the options to "Manage app" or "View". Click "View" to see your self-hosted instance of Volition. + +5. You're ready to start using your self-hosted instance of Volition! + +#### Note: Using this method to deploy means that your instance of Volition will always be the version it is when you deploy. We plan to add detailed instructions on forking/cloning the code base and deploying from that fork or clone in the near future. This will allow you to keep your instance up to date with the production version of Volition. diff --git a/app.json b/app.json index f72ae1e..e419b7e 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "name": "Volition", - "description": "Work in progress.", - "repository": "https://github.com/garrettqmartin8/volition", + "description": "Your Volition Instance", + "repository": "https://github.com/usevolition/volition", "scripts": { "postdeploy": "bundle exec rake db:migrate" }, @@ -18,15 +18,6 @@ "SMTP_USERNAME": { "value": "You can add this later." }, - "TWILIO_SID": { - "value": "Optional" - }, - "TWILIO_AUTH_TOKEN": { - "value": "Optional" - }, - "TWILIO_SENDER_PHONE_NUMBER": { - "value": "Optional" - }, "SELF_HOSTED": { "value": "true" } diff --git a/app/assets/images/apple-icon-114x114.png b/app/assets/images/apple-icon-114x114.png deleted file mode 100644 index 8cd405d..0000000 Binary files a/app/assets/images/apple-icon-114x114.png and /dev/null differ diff --git a/app/assets/images/apple-icon-144x144.png b/app/assets/images/apple-icon-144x144.png deleted file mode 100644 index 68250d1..0000000 Binary files a/app/assets/images/apple-icon-144x144.png and /dev/null differ diff --git a/app/assets/images/apple-icon-57x57.png b/app/assets/images/apple-icon-57x57.png deleted file mode 100644 index ab8c123..0000000 Binary files a/app/assets/images/apple-icon-57x57.png and /dev/null differ diff --git a/app/assets/images/apple-icon-72x72.png b/app/assets/images/apple-icon-72x72.png deleted file mode 100644 index a3f18ca..0000000 Binary files a/app/assets/images/apple-icon-72x72.png and /dev/null differ diff --git a/app/assets/images/apple-icon.png b/app/assets/images/apple-icon.png new file mode 100644 index 0000000..86fc593 Binary files /dev/null and b/app/assets/images/apple-icon.png differ diff --git a/app/assets/images/paid.png b/app/assets/images/paid.png new file mode 100644 index 0000000..20a8cec Binary files /dev/null and b/app/assets/images/paid.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 731ce71..5fd62ca 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -11,6 +11,7 @@ // about supported directives. // //= require jquery +//= require payola //= require jquery_ujs //= require turbolinks //= require react diff --git a/app/assets/javascripts/password.js b/app/assets/javascripts/password.js index b60ae0b..1f29bfa 100644 --- a/app/assets/javascripts/password.js +++ b/app/assets/javascripts/password.js @@ -1,13 +1,14 @@ $(document).on('turbolinks:load', function() { - if ($('body').hasClass('users-new')) { - $('#registration_password').focus(function(e) { + if ($('body').hasClass('users-new') || $('body').hasClass('passwords-edit')) { + $('#registration_password, #reset_password').focus(function(e) { $('.passwordRules').removeClass('hidden') }) - $('#registration_password').keyup(function(e) { + $('#registration_password, #reset_password').keyup(function(e) { var password = e.target.value + var rules = e.target.dataset.rules.split(",") - var passedRules = checkPasswordRules(password) + var passedRules = checkPasswordRules(password, rules) console.log(passedRules) var allRulesPassed = markPassedRules(passedRules) @@ -33,14 +34,12 @@ $(document).on('turbolinks:load', function() { } } - function checkPasswordRules(password) { - rules = [ - checkLength, - checkTop100, - ensureDoesntMatchEmail - ] + function checkPasswordRules(password, rules) { + var activeRules = rules.map(function(rule) { + return window[rule] + }) - return rules.map(function(rule, index) { + return activeRules.map(function(rule, index) { result = rule(password) if (result == true) { @@ -49,11 +48,11 @@ $(document).on('turbolinks:load', function() { }) } - function checkLength(password) { + window.checkLength = function(password) { return password.length >= 10 } - function checkTop100(password) { + window.checkTop100 = function(password) { if (!window.top100) { getTop100().then(function() { return !window.top100.includes(password) @@ -72,8 +71,8 @@ $(document).on('turbolinks:load', function() { }) } - function ensureDoesntMatchEmail(password) { - return $('#user_email').val() != password + window.ensureDoesntMatchEmail = function(password) { + return $('#registration_email').val() != password } } }) diff --git a/app/assets/javascripts/payment.js b/app/assets/javascripts/payment.js index 444c294..569dcc5 100644 --- a/app/assets/javascripts/payment.js +++ b/app/assets/javascripts/payment.js @@ -1,9 +1,31 @@ $(document).on('turbolinks:load', function() { - if ($('body').hasClass('payments-new')) { + if ($('body').hasClass('payments') || $('body').hasClass('gifts')) { var initializeStripe = (function(e) { - var stripe = Stripe(gon.stripe_public_key); - var elements = stripe.elements(); - var form = document.getElementById('payment-form') + var stripe = Stripe(gon.stripe_public_key); + var elements = stripe.elements(); + var form = document.getElementById('payment-form') + var planIdInput = document.getElementById('plan_id') + var monthlyPlanButton = document.getElementById("js--monthlyPlan") + var yearlyPlanButton = document.getElementById("js--yearlyPlan") + var selectedPlan = document.getElementById("selectedPlan") + + if ($('body').hasClass('payments-new')) { + planIdInput.value = gon.monthly_plan_id + + monthlyPlanButton.addEventListener('click', function(e) { + planIdInput.value = gon.monthly_plan_id + selectedPlan.innerHTML = "Monthly ($5.00)" + monthlyPlanButton.className = "" + yearlyPlanButton.className = "update" + }) + + yearlyPlanButton.addEventListener('click', function(e) { + planIdInput.value = gon.yearly_plan_id + selectedPlan.innerHTML = "Yearly ($50.00)" + yearlyPlanButton.className = "" + monthlyPlanButton.className = "update" + }) + } var style = { base: { diff --git a/app/assets/javascripts/settings.js b/app/assets/javascripts/settings.js new file mode 100644 index 0000000..6254828 --- /dev/null +++ b/app/assets/javascripts/settings.js @@ -0,0 +1,42 @@ +$(document).on('turbolinks:load', function() { + if ($('body').hasClass('users-edit')) { + var referralLink = document.getElementById("referral_link") + var copyButton = document.querySelector(".copyButton") + + referralLink.addEventListener("click", function(e) { + expandLink() + this.setSelectionRange(0, this.value.length) + + setTimeout(reset, 3000) + }) + + copyButton.addEventListener("click", function(e) { + expandLink() + referralLink.select() + + try { + var successful = document.execCommand('copy'); + + if (successful) { + copyButton.disabled = true + copyButton.innerHTML = "Copied!" + + setTimeout(reset, 3000) + } + } catch (err) { + console.log('Oops, unable to copy'); + } + }) + + var reset = function() { + copyButton.disabled = false + copyButton.innerHTML = "Copy and send link!" + referralLink.value = referralLink.dataset.referralCode + } + + var expandLink = function() { + referralLink.value = referralLink.dataset.referralLink + } + } +}) + diff --git a/app/assets/javascripts/week_plan.js b/app/assets/javascripts/week_plan.js index 01f7a48..a9f677f 100644 --- a/app/assets/javascripts/week_plan.js +++ b/app/assets/javascripts/week_plan.js @@ -1,6 +1,7 @@ $(document).on('click', '.js--toggleWeekPlan', function() { $('.addWeeklyTodos').toggleClass('hidden') $('.showWeekPlan').toggleClass('hidden') + $('.lastWeek').toggleClass('hidden') }) $(document).on('click', '.js--addWeeklyTodo', function() { diff --git a/app/assets/stylesheets/application/dashboard.scss b/app/assets/stylesheets/application/dashboard.scss index 097fee9..fbe0408 100644 --- a/app/assets/stylesheets/application/dashboard.scss +++ b/app/assets/stylesheets/application/dashboard.scss @@ -32,7 +32,7 @@ height: 100%; height: 100%; width: 75px; - padding: 20px 0; + padding: 20px; } .todayLink { @@ -42,10 +42,17 @@ .dashboardTodoList { font-size: 16px; @include flex(1); - padding-left: 50px; + padding-left: 20px; + width: 85%; } .dashboardTodo { + list-style-position:inside; + width: 80%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .complete { text-decoration: line-through; } @@ -62,7 +69,12 @@ } .time { - display: inline-block; + display: none; + + @include MQ(M) { + display: inline-block; + } + opacity: 0; transition: opacity 150ms; margin-left: 10px; diff --git a/app/assets/stylesheets/application/general.scss b/app/assets/stylesheets/application/general.scss index f39475a..1f3cacb 100644 --- a/app/assets/stylesheets/application/general.scss +++ b/app/assets/stylesheets/application/general.scss @@ -6,15 +6,26 @@ margin-bottom: 4rem; h1 { - font-size: 30px; + font-size: 2rem; color: $dark-gray; margin: 0; } h2 { - font-size: 20px; + font-size: 1.5rem; color: $green; } + + h3 { + font-size: 1.25rem; + color: $dark-gray; + font-weight: bold; + } + + h4 { + font-size: 1.1rem; + font-weight: bold; + } } blockquote { @@ -104,3 +115,33 @@ footer { color: $dark-heroku-purple; } } + +.flexTable { + display: flex; + flex-wrap: wrap; + margin: 0 0 3em 0; + padding: 0; + font-size: 1rem; +} +.flexTable-cell { + box-sizing: border-box; + flex-grow: 1; + width: 100%; // Default to full width + padding-right: 1.5em; + overflow: hidden; // Or flex might break + text-overflow: ellipsis; + list-style: none; + > h1, > h2, > h3, > h4, > h5, > h6 { margin: 0; } + + &.right { + text-align: right; + } +} + +/* Table column sizing +================================== */ +.flexTable--2cols > .flexTable-cell { width: 50%; } +.flexTable--3cols > .flexTable-cell { width: 33.33%; } +.flexTable--4cols > .flexTable-cell { width: 25%; } +.flexTable--5cols > .flexTable-cell { width: 20%; } +.flexTable--6cols > .flexTable-cell { width: 16.6%; } diff --git a/app/assets/stylesheets/application/marketing.scss b/app/assets/stylesheets/application/marketing.scss index f23ab9d..c602b20 100644 --- a/app/assets/stylesheets/application/marketing.scss +++ b/app/assets/stylesheets/application/marketing.scss @@ -154,7 +154,7 @@ text-align: center; font-size: 2rem; margin: 0 !important; - color: $light-blue; + color: $blue; } .sectionHeading { @@ -184,7 +184,10 @@ .hosted { background: $green; color: $white; - border-radius: $base-border-radius; + } + + .selfHosted { + background: lighten($heroku-purple, 35%); } .price { diff --git a/app/assets/stylesheets/application/navigation.scss b/app/assets/stylesheets/application/navigation.scss index c81bf12..213fc2b 100644 --- a/app/assets/stylesheets/application/navigation.scss +++ b/app/assets/stylesheets/application/navigation.scss @@ -51,7 +51,7 @@ @include align-self(flex-end); margin: 10px 40px 5px 0; font-size: 1rem; - color: $medium-gray; + color: $dark-gray; @include MQ(M) { display: none; @@ -62,7 +62,7 @@ @include display-flex; @include flex-direction(column-reverse); font-size: 1.2rem; - color: $lighter-blue; + color: $medium-gray; cursor: pointer; @include MQ(M) { @@ -75,8 +75,8 @@ .menuOverlay { width: 100vw !important; height: 100vh !important; - background: $light-blue; - opacity: 0.95; + background: $dark-gray; + opacity: .97; position: absolute; z-index: 1000; @include display-flex; diff --git a/app/assets/stylesheets/application/payments.scss b/app/assets/stylesheets/application/payments.scss index 0297729..3fc131b 100644 --- a/app/assets/stylesheets/application/payments.scss +++ b/app/assets/stylesheets/application/payments.scss @@ -5,3 +5,12 @@ width: 40%; } } + +.planOptions { + @include display-flex; + @include justify-content(space-between); + + width: 225px; + margin-bottom: 40px; +} + diff --git a/app/assets/stylesheets/application/reflections.scss b/app/assets/stylesheets/application/reflections.scss index 764703e..8596dff 100644 --- a/app/assets/stylesheets/application/reflections.scss +++ b/app/assets/stylesheets/application/reflections.scss @@ -31,15 +31,16 @@ width: 90%; @include MQ(M) { - width: initial; + width: 500px; } - .error { - padding: 10px; - border: 2px solid $red; - border-radius: $base-border-radius; - color: $red; - } +} + +.error { + padding: 10px; + border: 2px solid $red; + border-radius: $base-border-radius; + color: $red; } .reflectionDaySummary { @@ -54,3 +55,17 @@ font-size: 20px; } } + +.addToWeeklyPlan { + // background-color: $lightest-blue; +} + +.incompleteTodos { + font-size: 1rem; + padding: 0; + + li { + margin: 10px 0; + @include display-flex; + } +} diff --git a/app/assets/stylesheets/application/settings.scss b/app/assets/stylesheets/application/settings.scss index 9def10d..91b83fc 100644 --- a/app/assets/stylesheets/application/settings.scss +++ b/app/assets/stylesheets/application/settings.scss @@ -17,18 +17,70 @@ } } +.settingsWrapper { + width: 90%; + margin-top: 20px; + + @include MQ(M) { + width: 50%; + } +} + .settingsSection { width: 100%; margin-bottom: 60px; - &.dangerZone { - border: 2px solid $red; - } + .subscriptionCard { + width: 100%; + font-family: courier; + padding: 30px; + margin-bottom: 15px; + border-radius: $base-border-radius; + transform: rotate(-1deg); + box-shadow: 1px 1px $light-gray; + background-color: #ffeaa7; + // background-color: lighten($green, 40%); + + img { + display: none; + width: 20%; + vertical-align: top; + margin-right: 10px; + + @include MQ(S) { + display: inline-block; + } + } + + .headerText { + h4 { margin: 0; } + display: inline-block; + } + + hr { + border-bottom: 1px solid $base-font-color; + margin: 10px 0 10px 0; + } + + .cancelLink { + text-align: right; + color: $red; + font-size: .9rem; + text-decoration: underline; + text-decoration-skip: ink; + + &:hover, + &:active { + color: $dark-red; + } + } - .dangerZoneContent { - padding: 10px; } + hr { margin: 30px 0 20px 0; } + + .dangerZone { h2 { color: $red; } } + &.hostedFeature { padding: 40px 10px; border: 2px dashed $medium-gray; @@ -72,7 +124,7 @@ } .settingsInput { - margin-bottom: 40px; + margin-bottom: 15px; } .StripeElement { @@ -98,3 +150,33 @@ height: 175px !important; margin-right: 20px; } + +.referralWidget { + position: relative; + margin: 40px 0; + + .copyButton { + font-size: 14px; + position: absolute; + top: -15px; + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%) + } + + input { + padding: 30px 0; + background: lighten($green, 40%); + text-align: center; + font-family: courier; + font-size: 1rem; + border: 2px solid $green; + } +} + +.referredUsers { + .checkMark { + margin-left: 0; + margin-right: 10px; + } +} diff --git a/app/assets/stylesheets/application/week_plan.scss b/app/assets/stylesheets/application/week_plan.scss index 5182e3d..ed79e12 100644 --- a/app/assets/stylesheets/application/week_plan.scss +++ b/app/assets/stylesheets/application/week_plan.scss @@ -1,11 +1,11 @@ %weeklyTodos { position: relative; - background: $lightest-blue; - border: 2px solid $lighter-blue; + background: $blue; + border: 2px solid $blue; border-radius: $base-border-radius; color: $white; font-size: 1rem; - margin: 40px 0; + margin-top: 40px; height: 300px; .weeklyTodosScrollable { @@ -13,12 +13,12 @@ height: 100%; overflow-y: auto; &::-webkit-scrollbar { - background-color: $lightest-blue; + background-color: $blue; width: 5px; } &::-webkit-scrollbar-thumb { - background-color: $lighter-blue; + background-color: $dark-blue; border-radius: 3px; } } @@ -40,7 +40,7 @@ position: absolute; top: -20px; right: -2px; - background: $lighter-blue; + background: $blue; border-radius: 3px; width: 40px; height: 20px; @@ -65,6 +65,7 @@ .weeklyTodosSection { font-size: 1rem; + margin-bottom: 20px; width: 100%; @extend %weeklyTodos; @@ -89,24 +90,12 @@ cursor: default; transition: background 400ms; - // &:hover { // background: $lighter-blue; // } - .deleteTodoButton { - width: 20px; - height: 20px; - background: $white; - border-radius: $base-border-radius; - cursor: pointer; - @include display-flex; - @include align-items(center); - @include justify-content(center); - - i { - color: $red; - } + .button { + @include align-self(flex-start); } &.new { @@ -125,3 +114,11 @@ } } +div.showWeekPlan { + width: 100%; + text-align: center; + + .arrowDown { + font-size: 10px; + } +} diff --git a/app/assets/stylesheets/application/welcome.scss b/app/assets/stylesheets/application/welcome.scss index 73d8535..7a5ccbe 100644 --- a/app/assets/stylesheets/application/welcome.scss +++ b/app/assets/stylesheets/application/welcome.scss @@ -3,10 +3,6 @@ width: 600px; } -.welcomeUser { - color: $green; -} - .philosophy { font-size: 16px; list-style: disc; @@ -19,4 +15,3 @@ .garrett { width: 100px; } - diff --git a/app/assets/stylesheets/base/_buttons.scss b/app/assets/stylesheets/base/_buttons.scss index 26c1c8e..8d83aa0 100644 --- a/app/assets/stylesheets/base/_buttons.scss +++ b/app/assets/stylesheets/base/_buttons.scss @@ -16,7 +16,7 @@ button, .button, input[type="submit"] { text-decoration: none; transition: background-color $base-duration $base-timing, border $base-duration $base-timing; user-select: none; - vertical-align: middle; + // vertical-align: middle; white-space: nowrap; // text-shadow: 1px 1px 1px #666; @@ -28,7 +28,7 @@ button, .button, input[type="submit"] { &:focus { background-color: $dark-red; border: 2px solid $dark-red; - color: #fff; + color: $white; } } @@ -47,6 +47,7 @@ button, .button, input[type="submit"] { &.small { font-size: 1rem; + padding: 0.4em 0.5em !important; } &.extraSmall { @@ -68,13 +69,37 @@ button, .button, input[type="submit"] { background-color: transparent; border: 2px solid $green; padding: 0.6em $base-spacing; - color: $dark-gray; + color: $green; + + &:hover, + &:focus { + border: 2px solid $dark-green; + color: $dark-green; + } } &.light { background-color: $white; border: 2px solid $white; color: $green; + + &:hover, + &:focus { + background-color: darken(#fff, 10%); + border: 2px solid darken(#fff, 10%); + } + } + + &.danger { + border: 2px solid $red; + color: $red; + + &:hover, + &:active { + background: transparent; + border: 2px solid $dark-red; + color: $dark-red; + } } &.image { @@ -87,9 +112,30 @@ button, .button, input[type="submit"] { } &.blue { - background-color: $lightest-blue; - border: 2px solid $lighter-blue; + background-color: $blue; + border: 2px solid $blue; padding: 0.6em $base-spacing; + + &:hover, + &:focus { + background-color: $dark-blue; + border: 2px solid $dark-blue; + color: #fff; + } + } + + &.delete { + background-color: $red; + border: 2px solid $red; + padding: 0.6em $base-spacing; + color: $white; + + &:hover, + &:focus { + background-color: $dark-red; + border: 2px solid $dark-red; + color: $white; + } } } diff --git a/app/assets/stylesheets/base/_variables.scss b/app/assets/stylesheets/base/_variables.scss index 7dc8c6b..7a95aa6 100644 --- a/app/assets/stylesheets/base/_variables.scss +++ b/app/assets/stylesheets/base/_variables.scss @@ -11,7 +11,7 @@ $base-line-height: 1.5; $heading-line-height: 1.2; // Other Sizes -$base-border-radius: 2px; +$base-border-radius: 3px; $base-spacing: $base-line-height * 1em; $small-spacing: $base-spacing / 2; $base-z-index: 0; @@ -20,14 +20,12 @@ $base-z-index: 0; $dark-gray: #555; $medium-gray: #999; $light-gray: #ddd; -$red: #f44336; -$dark-red: darken($red, 5%); +$red: #e74c3c; +$dark-red: darken($red, 10%); $green: #49be5a; $dark-green: darken($green, 10%); -$blue: #25323c; -$light-blue: lighten($blue, 8%); -$lighter-blue: lighten($blue, 15%); -$lightest-blue: lighten($blue, 40%); +$blue: #4285F4; +$dark-blue: darken($blue, 10%); $white: #fff; $yellow: lighten(#ffff00, 35%); $heroku-purple: #7057bc; diff --git a/app/assets/stylesheets/flashes.scss b/app/assets/stylesheets/flashes.scss index f8b00b5..2d6d73c 100644 --- a/app/assets/stylesheets/flashes.scss +++ b/app/assets/stylesheets/flashes.scss @@ -16,6 +16,6 @@ } .flash-success { - border: 2px solid $lightest-blue; + border: 2px solid $blue; border-radius: $base-border-radius; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1a9a86d..bf65179 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,7 +36,7 @@ def authenticate_user! end def ensure_user_paid! - unless self_hosted? || current_user.paid? || current_user.trialing? + unless self_hosted? || current_user.active? || current_user.trialing? redirect_to new_payment_path end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 3e4d98d..f66f365 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -5,10 +5,13 @@ def show end unless params[:page] - @tomorrows_todo_list = TodoList.includes(:todos).tomorrow(current_user) + @tomorrows_todo_list = TodoList.includes(:daily_todos).tomorrow(current_user) end - @todays_todo_list = TodoList.includes(:todos, :user).today(current_user) - @past_todo_lists = TodoList.includes(:todos, :user).past(current_user).daily.paginate(page: params[:page]) + @todays_todo_list = TodoList.today(current_user)&.daily_snapshot || + TodoList.includes(:daily_todos, :user).today(current_user) + + @past_todo_lists = current_user.daily_snapshots.order(date: :desc) + .paginate(page: params[:page]) end end diff --git a/app/controllers/days_controller.rb b/app/controllers/days_controller.rb index 1dd36f5..a9b2593 100644 --- a/app/controllers/days_controller.rb +++ b/app/controllers/days_controller.rb @@ -1,6 +1,6 @@ class DaysController < AuthenticatedController def show - @todo_list = current_user.todo_lists.find_by(id: params[:id]) - @reflection = @todo_list.reflection + @daily_snapshot = current_user.daily_snapshots.find_by(date: params[:date]) + @reflection = @daily_snapshot.reflection end end diff --git a/app/controllers/gifts_controller.rb b/app/controllers/gifts_controller.rb new file mode 100644 index 0000000..f4343d0 --- /dev/null +++ b/app/controllers/gifts_controller.rb @@ -0,0 +1,54 @@ +class GiftsController < ApplicationController + def new + @gift = Gift.new + gon.stripe_public_key = ENV['STRIPE_PUBLIC_KEY'] + end + + def create + @gift = Gift.new(gift_params) + @gift.sender = current_user + + unless @gift.save + gon.stripe_public_key = ENV['STRIPE_PUBLIC_KEY'] + return render :new + end + + begin + charge_args = { + amount: 5_000, + currency: "usd", + description: "Gift Yearly Subscription from #{current_user.email}" + } + + payment_method = if current_user.active? + { + customer: current_user.subscription + .stripe_customer_id + } + else + { source: params[:stripeToken] } + end + + charge_args.merge!(payment_method) + + charge = Stripe::Charge.create(charge_args) + rescue => e + @gift.destroy + flash.now[:error] = "Something went wrong. Please check you card details and try again." + return render :new + end + + @gift.update(stripe_charge_id: charge.id) + + GiftsMailer.gift_notification(@gift).deliver_later + + flash[:success] = "You sent #{@gift.recipient_name} a yearly subscription!" + redirect_to settings_path + end + + private + + def gift_params + params.require(:gift).permit(:recipient_email, :recipient_name, :message) + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..4a840aa --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,56 @@ +class PasswordsController < ApplicationController + def new + end + + def create + if user = User.find_by(email: params[:email]) + user.forgot_password! + deliver_email(user) + end + + flash[:success] = "You will be sent a password recovery email shortly." + redirect_to login_path + end + + def edit + password_reset_token = if params[:token] + params[:token] + else + session[:password_reset_token] + end + + @user = User.find_by( + id: params[:id], + password_reset_token: password_reset_token + ) + + if @user && @user.eligible_for_password_reset + session[:password_reset_token] = params[:token] + else + flash[:error] = "Either that user doesn't exist or your token has expired." + redirect_to login_path + end + end + + def update + @user = User.find_by(password_reset_token: session[:password_reset_token]) + @user.password = params[:password] + + if @user.save + flash[:success] = "Password successfully reset. Please log in to continue." + redirect_to login_path + else + render :edit + end + end + + private + + def deliver_email(user) + PasswordsMailer.change_password(user).deliver_later + end + + def password_reset_params + + end +end diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 2e96a34..0d23f1b 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -1,30 +1,87 @@ class PaymentsController < ApplicationController - before_action :ensure_not_paid! + before_action :ensure_not_paid!, only: [:new, :create] def new + @monthly_plan = SubscriptionPlan.find_by(name: "Monthly") + @yearly_plan = SubscriptionPlan.find_by(name: "Yearly") + @subscription = current_user.subscription + gon.stripe_public_key = ENV['STRIPE_PUBLIC_KEY'] + gon.monthly_plan_id = @monthly_plan.id + gon.yearly_plan_id = @yearly_plan.id end def create - valid = PaymentService.charge_card( - token: params[:stripeToken], - user: current_user + plan = SubscriptionPlan.find_by(id: params[:plan_id]) + + subscription = Payola::Subscription.create( + plan: plan, + email: current_user.email, + stripe_token: params[:stripeToken], + currency: "usd", + owner: current_user, + amount: plan.amount ) - if valid + subscription.process! + + if subscription.active? + activate_referral if current_user.referrer.present? redirect_to thank_you_path else flash[:error] = %( - Something went wrong. Please check you card details and try again + Something went wrong. Please check your card details and try again ) + subscription.destroy + redirect_to new_payment_path end end + def edit + gon.stripe_public_key = ENV['STRIPE_PUBLIC_KEY'] + @subscription = current_user.subscription + end + + def update + @subscription = current_user.subscription + @customer = Stripe::Customer.retrieve(@subscription.stripe_customer_id) + @customer.source = params[:stripeToken] + + @customer.save + + source = @customer.sources.first + + @subscription.update( + stripe_token: params[:stripeToken], + card_last4: source.last4, + card_expiration: Date.new(source.exp_year, source.exp_month, 1) + ) + + flash[:success] = "Updated billing info" + redirect_to settings_path + end + private + def activate_referral + referrer = current_user.referrer + subscription_id = referrer.subscription.stripe_id + coupon_name = if referrer.subscription.name == "Monthly" + "referral_credit_monthly" + else + "referral_credit_yearly" + end + + stripe_subscription = Stripe::Subscription.retrieve(subscription_id) + stripe_subscription.coupon = coupon_name + stripe_subscription.save + + PaymentsMailer.referral_activated(current_user).deliver_later + end + def ensure_not_paid! - redirect_to settings_path if current_user.paid? + redirect_to settings_path if current_user.active? end end diff --git a/app/controllers/preferences_controller.rb b/app/controllers/preferences_controller.rb new file mode 100644 index 0000000..d6149f3 --- /dev/null +++ b/app/controllers/preferences_controller.rb @@ -0,0 +1,56 @@ +class PreferencesController < ApplicationController + before_action :set_user + + def update + @user.skip_password_validation = true + @user.assign_attributes({ + track_weekends: params[:track_weekends], + weekly_summary: params[:weekly_summary] + }) + + if @user.save + flash[:success] = "Preferences saved!" + else + flash[:error] = "Something went wrong. Please try again." + end + + redirect_to settings_path + end + + def update_email + authenticate_with(params[:password]) + + if @user.update(email: params[:email]) + flash[:success] = "Email updated!" + else + flash[:error] = "Something went wrong. Please try again." + end + + redirect_to settings_path + end + + def update_password + authenticate_with(params[:current_password]) + + if @user.update(password: params[:new_password]) + flash[:success] = "Password updated!" + else + flash[:error] = "Something went wrong. Please try again." + end + + redirect_to settings_path + end + + private + + def authenticate_with(password) + unless @user.authenticate(password) + flash[:error] = "Incorrect password" + redirect_to settings_path + end + end + + def set_user + @user = current_user + end +end diff --git a/app/controllers/reflections_controller.rb b/app/controllers/reflections_controller.rb index 0cf3ba1..6d0ec1c 100644 --- a/app/controllers/reflections_controller.rb +++ b/app/controllers/reflections_controller.rb @@ -2,27 +2,46 @@ class ReflectionsController < AuthenticatedController include ApplicationHelper def new - if Reflection.today(current_user).present? + @reflection = Reflection.new + @todo_list = current_user.todo_lists.daily.find_by(date: params[:date]) || + TodoList.today(current_user) + + @daily_snapshot = @todo_list.daily_snapshot + @reflecting_on_today = @todo_list == TodoList.today(current_user) + @link_text, @link_url = if @reflecting_on_today + ["Go back to today's tasks", today_path] + else + ["Go back", day_path(@daily_snapshot.date)] + end + + if Reflection.today(current_user).present? && @reflecting_on_today flash[:error] = 'You already wrote your reflection for today.' redirect_to dashboard_path - elsif TodoList.today(current_user).blank? + elsif TodoList.today(current_user).blank? && @reflecting_on_today flash[:error] = 'You must start your tasks for today before writing a reflection.' redirect_to dashboard_path end - - @reflection = Reflection.new - @todo_list = TodoList.today(current_user) end def create @reflection = Reflection.new(reflection_params) @reflection.user = current_user - @reflection.date = Date.current + + @todo_list = current_user.todo_lists.daily.find_by(date: reflection_params[:date]) + @todo_list.daily_snapshot&.destroy if @reflection.save + @daily_snapshot = DailySnapshot.create_from_todo_list(@todo_list) + + Todo.where(id: params[:add_to_week]).each do |todo| + todo.update( + weekly_todo_list_id: current_week_plan.id, + daily_todo_list_id: nil + ) + end + redirect_to after_create_path else - @todo_list = TodoList.today(current_user) render :new end end @@ -30,7 +49,9 @@ def create private def after_create_path - if tomorrow_is_trackable? + if @reflection.date != Date.current + day_path(@daily_snapshot.date) + elsif tomorrow_is_trackable? tomorrow_path else flash[:success] = 'Nice job today! Get some rest.' @@ -39,6 +60,12 @@ def after_create_path end def reflection_params - params.require(:reflection).permit(:rating, :positive, :negative, :undone) + params.require(:reflection).permit( + :rating, + :positive, + :negative, + :undone, + :date + ) end end diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb new file mode 100644 index 0000000..18473cc --- /dev/null +++ b/app/controllers/subscriptions_controller.rb @@ -0,0 +1,11 @@ +class SubscriptionsController < AuthenticatedController + def destroy + @subscription = current_user.subscription + @subscription.cancel! + + Stripe::Subscription.retrieve(@subscription.stripe_id).delete + @subscription.destroy + + redirect_to edit_user_path(current_user) + end +end diff --git a/app/controllers/today_controller.rb b/app/controllers/today_controller.rb index 4ff2547..550584c 100644 --- a/app/controllers/today_controller.rb +++ b/app/controllers/today_controller.rb @@ -6,15 +6,18 @@ def show @week_plan = current_week_plan @reflection = Reflection.today(current_user) @todo_list = TodoList.today(@user) - @button_text, @button_path = if Reflection.today(current_user).present? - ["Plan for tomorrow", tomorrow_path] - else - ["Reflect on your day", reflect_path] - end if @todo_list.blank? redirect_to new_today_path else + @button_text, @button_path = if Reflection.today(current_user).present? + ["Plan for tomorrow", tomorrow_path] + else + [ + "Reflect on your day", + reflect_path(@todo_list.date), + ] + end @todos = @todo_list.todos.frontend_info end end @@ -24,10 +27,10 @@ def new redirect_to today_path end - @week_plan = current_week_plan @todo_list = TodoList.new + @week_plan = current_week_plan 5.times do - @todo_list.todos.build + @todo_list.daily_todos.new end end @@ -36,14 +39,16 @@ def create date: Date.current, user: @user, list_type: 'daily', - week_plan: current_week_plan + week_plan: current_week_plan, ) - if @todo_list.save - @todo_list.update(todos_attributes: todo_list_params[:todos_attributes]) + @todo_list.update(daily_todos_attributes: daily_todo_list_params[:daily_todos_attributes]) + overlapping_todo_content = @todo_list.todos.pluck(:content) & current_week_plan.todos.pluck(:content) + if overlapping_todo_content.size > 0 + current_week_plan.todos.where(content: overlapping_todo_content).destroy_all + end redirect_to today_path end - end private @@ -59,7 +64,7 @@ def set_user end end - def todo_list_params - params.require(:todo_list).permit(todos_attributes: [:content, :estimated_time_blocks]) + def daily_todo_list_params + params.require(:todo_list).permit(daily_todos_attributes: [:content, :estimated_time_blocks]) end end diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 1c4bd2a..fd7a7a9 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -6,6 +6,19 @@ def update begin @todo.update!(todo_params) + + todo_list = @todo.daily_todo_list + todo_in_week_plan = current_week_plan.todos.find_by(content: @todo.content) + + if todo_in_week_plan.present? + todo_in_week_plan.destroy + end + + if todo_list.daily_snapshot.present? + todo_list.daily_snapshot.destroy + DailySnapshot.create_from_todo_list(todo_list) + end + render json: { saved: true } rescue => e puts e diff --git a/app/controllers/tomorrow_controller.rb b/app/controllers/tomorrow_controller.rb index b67a565..66bcde5 100644 --- a/app/controllers/tomorrow_controller.rb +++ b/app/controllers/tomorrow_controller.rb @@ -5,7 +5,7 @@ def new message = "You already planned tomorrow\'s tasks. You can change them when you start your day." dashboard_path else - message = "You must write a reflection for today before planning tomorrow\'s tasks." + message = "You must write a reflection for today before planning tomorrow\'s tasks." reflect_path end @@ -19,7 +19,7 @@ def new @todo_list = TodoList.new @week_plan = current_week_plan 5.times do - @todo_list.todos.build + @todo_list.daily_todos.new end end @@ -30,7 +30,11 @@ def create ) if @todo_list.save - @todo_list.update(todos_attributes: todo_list_params[:todos_attributes]) + @todo_list.update(daily_todos_attributes: daily_todo_list_params[:daily_todos_attributes]) + overlapping_todo_content = @todo_list.todos.pluck(:content) & current_week_plan.todos.pluck(:content) + if overlapping_todo_content.size > 0 + current_week_plan.todos.where(content: overlapping_todo_content).destroy_all + end redirect_to after_create_path end @@ -57,7 +61,7 @@ def after_create_path end end - def todo_list_params - params.require(:todo_list).permit(todos_attributes: [:content, :estimated_time_blocks]) + def daily_todo_list_params + params.require(:todo_list).permit(daily_todos_attributes: [:content, :estimated_time_blocks]) end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d344070..f9fb4d7 100755 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -5,6 +5,14 @@ class UsersController < AuthenticatedController before_action :set_user def new + if params[:referral_code] && user_with_referral_code_exists?(params[:referral_code]) + flash.now[:success] = "You got a referral credit! Enjoy one month of Volition, on us." + session[:referral_code] = params[:referral_code] + elsif params[:gift_token] && gift_with_token_exists?(params[:gift_token]) + flash.now[:success] = "You have redeemed your free 1-year subscription! Create your account to begin using Volition." + session[:gift_token] = params[:gift_token] + end + if current_user.present? && !current_user.guest? redirect_to dashboard_path end @@ -13,7 +21,13 @@ def new def create valid_params = params[:google_id_token].present? ? params : registration_params - @registration = Registration.new(valid_params, current_user || User.new) + valid_params.merge!({ + referral_code: session[:referral_code], + gift_token: session[:gift_token] + }) + + @registration = Registration.new(valid_params, + current_user || User.new) if @registration.save login(@registration.user) @@ -25,6 +39,7 @@ def create end def edit + @subscription = current_user.subscription end def update @@ -58,6 +73,14 @@ def destroy private + def user_with_referral_code_exists?(referral_code) + User.find_by(referral_code: referral_code).present? + end + + def gift_with_token_exists?(gift_token) + Gift.find_by(unique_token: gift_token, recipient_id: nil).present? + end + def add_card_to_user @payment_service = PaymentService.new({ stripe_customer_id: @user.stripe_customer_id }) @@ -76,9 +99,6 @@ def registration_params params.require(:registration).permit( :name, :email, - :phone, - :email_reminders, - :sms_reminders, :track_weekends, :password, :google_id @@ -89,9 +109,6 @@ def user_params params.require(:user).permit( :name, :email, - :phone, - :email_reminders, - :sms_reminders, :track_weekends, :password, :google_id, diff --git a/app/controllers/week_plan_controller.rb b/app/controllers/week_plan_controller.rb index 801419b..a8e102e 100644 --- a/app/controllers/week_plan_controller.rb +++ b/app/controllers/week_plan_controller.rb @@ -1,13 +1,14 @@ class WeekPlanController < AuthenticatedController def show - @week_plan = current_week_plan + @last_week = get_last_week + @week_plan = get_current_or_upcoming_week end def add_todo - @week_plan = current_week_plan - @todo = Todo.create( + @week_plan = get_current_or_upcoming_week + @todo = Todo.create!( content: params[:content], - todo_list: @week_plan + weekly_todo_list: @week_plan ) respond_to do |format| @@ -16,7 +17,7 @@ def add_todo end def remove_todo - @week_plan = current_week_plan + @week_plan = get_current_or_upcoming_week @todo = @week_plan.todos.find_by(id: params[:id]) if @todo @@ -27,4 +28,45 @@ def remove_todo format.js end end + + def move_todo + @last_week = get_last_week + @week_plan = get_current_or_upcoming_week + @todo = @last_week.todos.find_by(id: params[:id]) + + if @todo + @todo.update( + weekly_todo_list_id: @week_plan.id, + daily_todo_list: nil + ) + end + + respond_to do |format| + format.js + end + end + + private + + def get_last_week + if Date.current.sunday? + current_week_plan + else + beginning_of_last_week = Date.current.last_week + current_user.todo_lists + .weekly + .find_by(date: beginning_of_last_week) + end + end + + def get_current_or_upcoming_week + if Date.current.sunday? + current_user.todo_lists.find_or_create_by( + list_type: "weekly", + date: ((@last_week&.date || Date.current) + 1.week).beginning_of_week + ) + else + current_week_plan + end + end end diff --git a/app/forms/registration.rb b/app/forms/registration.rb index b7c841d..5a9385d 100644 --- a/app/forms/registration.rb +++ b/app/forms/registration.rb @@ -18,6 +18,9 @@ def initialize(params = {}, user = User.new) end def save + referral_code = @params.delete(:referral_code) + gift_token = @params.delete(:gift_token) + if signing_up_with_google? @user.skip_password_validation = true google_identity = GoogleSignIn::Identity.new(params[:google_id_token]) @@ -38,11 +41,29 @@ def save @user.timezone = Time.zone.tzinfo.name add_to_mailchimp_newsletter + note_referral(referral_code) + redeem_gift(gift_token) @user.save end private + def note_referral(referral_code) + referrer = User.find_by(referral_code: referral_code) + return unless referrer + @user.referred_by = referrer.id + end + + def redeem_gift(gift_token) + gift = Gift.find_by(unique_token: gift_token) + + return unless gift + + gift.update(recipient: @user) + + CreateStripeGiftSubscriptionJob.perform_later(gift) + end + def add_to_mailchimp_newsletter return nil if self_hosted? || @user.guest? || Rails.env.development? || Rails.env.test? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 66f4f40..9cbdde8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -23,14 +23,6 @@ def tomorrow_is_trackable? end end - def truncate(string, user_agent: :desktop) - if user_agent == :desktop - string.truncate(40) - else - string.truncate(25) - end - end - def self_hosted? ENV['SELF_HOSTED'] == 'true' end @@ -38,9 +30,4 @@ def self_hosted? def on_home_page? controller.action_name == 'home' end - - def eligible_for_daily_reminders? - ENV["SMTP_ADDRESS"] != "You can add this later." && - ENV["TWILIO_SID"] != "Optional" - end end diff --git a/app/jobs/create_daily_snapshots_job.rb b/app/jobs/create_daily_snapshots_job.rb new file mode 100644 index 0000000..c1db33d --- /dev/null +++ b/app/jobs/create_daily_snapshots_job.rb @@ -0,0 +1,10 @@ +class CreateDailySnapshotsJob < ApplicationJob + queue_as :daily_snapshots + + def perform + TodoList.where(user_id: User.finishing_their_day.pluck(:id)) + .missing_snapshot.find_each do |todo_list| + DailySnapshot.create_from_todo_list(todo_list) + end + end +end diff --git a/app/jobs/create_stripe_gift_subscription_job.rb b/app/jobs/create_stripe_gift_subscription_job.rb new file mode 100644 index 0000000..e8ca07c --- /dev/null +++ b/app/jobs/create_stripe_gift_subscription_job.rb @@ -0,0 +1,33 @@ +class CreateStripeGiftSubscriptionJob < ApplicationJob + queue_as :create_gift_subscription + + def perform(gift) + recipient = gift.recipient + + stripe_customer = Stripe::Customer.create( + { email: recipient.email }, + # { idempotency_key: gift.unique_token } + ) + + plan = SubscriptionPlan.find_by(name: "Yearly") + + stripe_subscription = Stripe::Subscription.create( + customer: stripe_customer.id, + items: [{ plan: "yearly" }], + coupon: "gift_yearly" + ) + + payola_subscription = Payola::Subscription.create!( + plan: plan, + email: recipient.email, + currency: "usd", + owner: recipient, + coupon: "gift_yearly", + stripe_customer_id: stripe_customer.id, + stripe_id: stripe_subscription.id + ) + + payola_subscription.sync_with!(stripe_subscription) + payola_subscription.update(state: "active", coupon: "gift_yearly") + end +end diff --git a/app/jobs/email_reminders_job.rb b/app/jobs/email_reminders_job.rb deleted file mode 100644 index 8a0dd2d..0000000 --- a/app/jobs/email_reminders_job.rb +++ /dev/null @@ -1,11 +0,0 @@ -class EmailRemindersJob < ApplicationJob - queue_as :email_reminders - - def perform - User.want_email_reminders.finishing_their_day.each do |user| - RemindersMailer.send_reminder_to(user).deliver - end - rescue => e - Rails.logger.error(e) - end -end diff --git a/app/jobs/sms_reminders_job.rb b/app/jobs/sms_reminders_job.rb deleted file mode 100644 index 5b5d65b..0000000 --- a/app/jobs/sms_reminders_job.rb +++ /dev/null @@ -1,20 +0,0 @@ -class SmsRemindersJob < ApplicationJob - queue_as :sms_reminders - - def perform - twilio_client = Twilio::REST::Client.new( - ENV['TWILIO_SID'], - ENV['TWILIO_AUTH_TOKEN'] - ) - - User.want_sms_reminders.finishing_their_day.each do |user| - twilio_client.account.messages.create({ - from: ENV['TWILIO_SENDER_PHONE_NUMBER'], - to: user.phone, - body: 'Here\'s a reminder from Volition to reflect on your day.' - }) - end - rescue => e - Rails.logger.error(e) - end -end diff --git a/app/mailers/gifts_mailer.rb b/app/mailers/gifts_mailer.rb new file mode 100644 index 0000000..fd9aa2b --- /dev/null +++ b/app/mailers/gifts_mailer.rb @@ -0,0 +1,13 @@ +class GiftsMailer < ApplicationMailer + default from: "gifts@usevolition.com" + + def gift_notification(gift) + @gift = gift + @sender = @gift.sender + + mail( + to: @gift.recipient_email, + subject: "[Volition] You have received a gift from #{@sender.name}" + ) + end +end diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb new file mode 100644 index 0000000..ac943ab --- /dev/null +++ b/app/mailers/passwords_mailer.rb @@ -0,0 +1,8 @@ +class PasswordsMailer < ApplicationMailer + default from: 'passwords@usevolition.com' + def change_password(user) + @user = user + + mail(to: user.email, subject: "[Volition] Password reset") + end +end diff --git a/app/mailers/payments_mailer.rb b/app/mailers/payments_mailer.rb index b46bdf7..8842109 100644 --- a/app/mailers/payments_mailer.rb +++ b/app/mailers/payments_mailer.rb @@ -1,13 +1,23 @@ class PaymentsMailer < ApplicationMailer default from: 'payments@usevolition.com' - def send_trial_ending_to(user) - @user = user - @subscription_end_date = Date.strptime(user.stripe_subscription.trial_end.to_s, '%s') + def invoice_upcoming(subscription) + @subscription = subscription + @user = @subscription.owner mail( to: @user.email, - subject: '[Volition] Your trial is ending soon!' + subject: "[Volition] You card will be charged soon" + ) + end + + def referral_activated(referred_user) + @referred_user = referred_user + @referrer = referred_user.referrer + + mail( + to: @referrer.email, + subject: "[Volition] Someone used your referral link!" ) end end diff --git a/app/mailers/reminders_mailer.rb b/app/mailers/reminders_mailer.rb deleted file mode 100644 index 1d4c631..0000000 --- a/app/mailers/reminders_mailer.rb +++ /dev/null @@ -1,11 +0,0 @@ -class RemindersMailer < ApplicationMailer - default from: 'reminders@usevolition.com' - - def send_reminder_to(user) - @user = user - mail( - to: @user.email, - subject: '[Volition] Reminder to reflect' - ) - end -end diff --git a/app/mailers/weekly_summary_mailer.rb b/app/mailers/weekly_summary_mailer.rb index 0904658..871138d 100644 --- a/app/mailers/weekly_summary_mailer.rb +++ b/app/mailers/weekly_summary_mailer.rb @@ -2,42 +2,24 @@ class WeeklySummaryMailer < ApplicationMailer default from: "summary@usevolition.com" def weekly_summary(user) + from = 6.days.ago + to = Date.current.end_of_day @user = user - date_args_last_week = { from: 12.days.ago, to: 6.days.ago } - date_args_this_week = { from: 6.days.ago, to: Date.current.end_of_day } - @completion_percentage = user.completion_percentage(**date_args_this_week) - @average_rating = user.average_rating(**date_args_this_week) - @completion_percentage_last_week = user.completion_percentage(**date_args_last_week) - @average_rating_last_week = user.average_rating(**date_args_last_week) - @completion_percentage_arrow, @completion_percentage_class = if @completion_percentage > @completion_percentage_last_week - ["▲", "better"] - elsif @completion_percentage < @completion_percentage_last_week - ["▼", "worse"] - else - "−" - end + @weekly_summary = user.weekly_summaries.find_or_create_by(created_at: Date.current.beginning_of_day..Time.current) + @weekly_summary.update( + todo_list_ids: user.todo_lists.where(created_at: from..to).pluck(:id) + ) + @weekly_summary.generate_stats - @average_rating_arrow, @average_rating_class = if @average_rating > @average_rating_last_week - ["▲", "better"] - elsif @average_rating < @average_rating_last_week - ["▼", "worse"] - else - "−" - end + @completion_percentage_arrow, @completion_percentage_class = @weekly_summary.arrow_and_class(:completion_percentage) - @recommendation = if @completion_percentage < 70 && @average_rating < 7 - "It looks like you didn't get everything done this past week and that reflected on how productive you thought your days were. Try being more deliberate in planning your tasks this coming week in order to build momentum." - elsif @completion_percentage >= 70 && @average_rating < 7 - "You did well completing your tasks this week, but felt you could have been more productive. You only have five tasks to plan in Volition for a day. Make sure these tasks are the ones that will make you feel like you're making the most progress in your work and life." - elsif @completion_percentage < 70 && @average_rating >= 7 - "It seems like you felt productive this week, but you didn't complete all the tasks you planned to. Try to break your tasks into smaller pieces this upcoming week." - elsif @completion_percentage >= 70 && @average_rating >= 7 - "Great job! You made a solid plan and you stuck to it. Keep up the good work!" - end + @average_rating_arrow, @average_rating_class = @weekly_summary.arrow_and_class(:weekly_rating) - reflections = user.reflections.where(created_at: date_args_this_week[:from]..date_args_this_week[:to]) - @negative = reflections.pluck(:negative) - @positive = reflections.pluck(:positive) + @recommendation = @weekly_summary.recommendation + + reflections = @weekly_summary.reflections + @negative = reflections.map(&:negative).uniq + @positive = reflections.map(&:positive).uniq mail(to: @user.email, subject: "[Volition] Your weekly summary") end diff --git a/app/models/concerns/data_struct.rb b/app/models/concerns/data_struct.rb new file mode 100644 index 0000000..8497667 --- /dev/null +++ b/app/models/concerns/data_struct.rb @@ -0,0 +1,9 @@ +class DataStruct < OpenStruct + def method_missing(method_name, *arguments, &block) + if @table[:table].respond_to?(method_name) + @table[:table].send(method_name) + else + super + end + end +end diff --git a/app/models/daily_snapshot.rb b/app/models/daily_snapshot.rb new file mode 100644 index 0000000..158243d --- /dev/null +++ b/app/models/daily_snapshot.rb @@ -0,0 +1,44 @@ +class DailySnapshot < ApplicationRecord + belongs_to :todo_list + + serialize :data, DataStructSerializer + + delegate :reflection, to: :todo_list + + validates :todo_list, uniqueness: true + + scope :today, -> { where(date: Date.current.beginning_of_day..Time.current) } + + def todos + data.todos + end + + def self.create_from_todo_list(todo_list) + unless todo_list.daily? + raise StandardError, I18n.t("daily_snapshot.weekly_todo_list_error_message") + end + + columns_to_select = %w( + content + estimated_time_blocks + actual_time_blocks + complete + ) + + create!( + todo_list_id: todo_list.id, + date: todo_list.date, + data: { todos: Todo.select(columns_to_select) + .where(daily_todo_list_id: todo_list.id) + .as_json(except: :id) } + ) + end + + def completion_percentage + return 0 unless todos.present? + todos_filled_out = todos.select { |todo| todo.content.present? } + return 0 unless todos_filled_out.present? + + (todos_filled_out.select { |todo| todo.complete }.count / todos_filled_out.count.to_f) * 100 + end +end diff --git a/app/models/gift.rb b/app/models/gift.rb new file mode 100644 index 0000000..0670269 --- /dev/null +++ b/app/models/gift.rb @@ -0,0 +1,16 @@ +# Purchased and sent to someone that doesn't currently subscribed to Volition +# When a user signs up through the unique gift link, a discounted Stripe +# subscription is created. + +class Gift < ApplicationRecord + with_options class_name: "User" do + belongs_to :recipient, required: false + belongs_to :sender + end + + has_secure_token :unique_token + + validates :recipient_email, presence: true + validates :recipient_name, presence: true + validates :message, presence: true +end diff --git a/app/models/null_subscription.rb b/app/models/null_subscription.rb new file mode 100644 index 0000000..39ed933 --- /dev/null +++ b/app/models/null_subscription.rb @@ -0,0 +1,9 @@ +class NullSubscription + def active? + false + end + + def stripe_id + nil + end +end diff --git a/app/models/subscription_plan.rb b/app/models/subscription_plan.rb new file mode 100644 index 0000000..9882def --- /dev/null +++ b/app/models/subscription_plan.rb @@ -0,0 +1,7 @@ +class SubscriptionPlan < ApplicationRecord + include Payola::Plan + + def redirect_path(subscription) + "/settings" + end +end diff --git a/app/models/todo.rb b/app/models/todo.rb index 5bc5c7b..5c036ef 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,11 +1,25 @@ class Todo < ApplicationRecord - belongs_to :todo_list + with_options class_name: 'TodoList', required: false do |options| + options.belongs_to :daily_todo_list, foreign_key: 'daily_todo_list_id' + options.belongs_to :weekly_todo_list, foreign_key: 'weekly_todo_list_id' + end default_scope { order(id: :asc) } - validates :content, presence: true, if: -> { todo_list.weekly? } + validates :content, presence: true, if: -> { weekly_todo_list.present? } scope :complete, -> { where(complete: true) } + scope :incomplete, -> { where(complete: false).where.not(content: "") } + scope :for_todo_list, -> (todo_list_id) { + ids = find_by_sql %( + select id from todos + where daily_todo_list_id = #{todo_list_id} + or weekly_todo_list_id = #{todo_list_id}; + ).squish + + where(id: ids) + } + scope :frontend_info, -> { select(:actual_time_blocks, :estimated_time_blocks, @@ -13,4 +27,5 @@ class Todo < ApplicationRecord :content, :id) } + end diff --git a/app/models/todo_list.rb b/app/models/todo_list.rb index d28b66c..f5e611a 100644 --- a/app/models/todo_list.rb +++ b/app/models/todo_list.rb @@ -1,20 +1,43 @@ class TodoList < ApplicationRecord extend Enumerize - has_many :todos, dependent: :delete_all + before_destroy :delete_todos + + with_options class_name: 'Todo', dependent: :destroy do |options| + options.has_many :daily_todos, foreign_key: :daily_todo_list_id + options.has_many :weekly_todos, foreign_key: :weekly_todo_list_id + end + + def todos + daily_todos.or(weekly_todos) + end belongs_to :user belongs_to :week_plan, class_name: 'TodoList', required: false - accepts_nested_attributes_for :todos, + has_one :daily_snapshot, dependent: :destroy + + accepts_nested_attributes_for :daily_todos, reject_if: :all_blank + accepts_nested_attributes_for :weekly_todos, + reject_if: :all_blank + + self.per_page = 5 enumerize :list_type, in: %w(daily weekly) scope :weekly, -> { where(list_type: 'weekly') } scope :daily, -> { where(list_type: 'daily') } + scope :missing_snapshot, -> { + daily.left_outer_joins(:daily_snapshot) + .where(daily_snapshots: { id: nil }) + } + + def delete_todos + todos.delete_all + end def weekly? list_type == 'weekly' diff --git a/app/models/user.rb b/app/models/user.rb index 2272d7c..256cfad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,20 +1,39 @@ class User < ApplicationRecord + include Rails.application.routes.url_helpers + has_many :reflections, dependent: :destroy has_many :todo_lists, dependent: :destroy - has_many :todos, through: :todo_lists + has_many :daily_todos, through: :todo_lists + has_many :weekly_todos, through: :todo_lists + has_many :daily_snapshots, -> { where.not(date: Date.current) }, through: :todo_lists + has_many :weekly_summaries + + with_options class_name: "User", foreign_key: :referred_by do + has_many :referred_users + + belongs_to :referrer, optional: true + end + + has_one :subscription, + foreign_key: "owner_id", + class_name: "Payola::Subscription" + + delegate :active?, to: :subscription + + def subscription + super || NullSubscription.new + end has_secure_password validations: false + has_secure_token :referral_code - scope :want_sms_reminders, -> { where(sms_reminders: true) } - scope :want_email_reminders, -> { where(email_reminders: true) } - scope :want_weekly_summaries, -> { where(weekly_summary: true) } - scope :paid, -> { where(paid: true) } + scope :want_weekly_summaries, -> { where(weekly_summary: true) } + scope :paid, -> { where(paid: true) } - validates :phone, presence: true, if: -> { self.sms_reminders? } validates :email, presence: true, unless: -> { self.guest? } validates :email, uniqueness: true, unless: -> { self.guest? } validates :password_digest, presence: true, unless: :skip_password_validation - validate :validate_password + # validate :validate_password attr_accessor :skip_password_validation @@ -49,22 +68,46 @@ def recently_signed_up? end def trialing? - reflections.count < 2 + reflections.count < 2 || in_referral_month? + end + + def in_referral_month? + return false unless referred_by.present? + + Date.current < (created_at + 1.month) end def completion_percentage(from:, to:) - list_ids = todo_lists.daily.where(created_at: from..to).pluck(:id) - all_todos = todos.where(todo_list_id: list_ids) - complete_todos = all_todos.merge(Todo.complete) + snapshots = DailySnapshot.where( + created_at: from..to, + todo_list_id: todo_lists.pluck(:id), + ) - if all_todos.count == 0 && complete_todos.count == 0 - return 0 - end + return 0 if snapshots.blank? - ((complete_todos.count.to_f / all_todos.count) * 100).round(2) + snapshots.reduce(0) do |sum, snapshot| + next sum unless snapshot.completion_percentage.is_a?(Integer) + sum += snapshot.completion_percentage + sum + end / snapshots.size end def average_rating(from:, to:) (reflections.where(created_at: from..to).average(:rating) || 0).to_i end + + def forgot_password! + update( + password_reset_token: SecureRandom.hex, + password_reset_token_expiration: Time.current + 2.hours + ) + end + + def eligible_for_password_reset + password_reset_token_expiration > Time.current + end + + def referral_link + new_user_url(referral_code: referral_code, host: ENV["APPLICATION_HOST"]) + end end diff --git a/app/models/weekly_summary.rb b/app/models/weekly_summary.rb new file mode 100644 index 0000000..93f2dfa --- /dev/null +++ b/app/models/weekly_summary.rb @@ -0,0 +1,57 @@ +class WeeklySummary < ApplicationRecord + belongs_to :user + + def todo_lists + TodoList.where(id: todo_list_ids) + end + + def daily_snapshots + DailySnapshot.where(todo_list_id: todo_list_ids) + end + + def reflections + todo_lists.map(&:reflection).compact + end + + def previous_weekly_summary + (user.weekly_summaries.where.not(id: id)).last + end + + def generate_stats + weekly_todos = daily_snapshots.flat_map(&:todos) + complete_todos = weekly_todos.select(&:complete) + self.completion_percentage = (complete_todos.count / weekly_todos.count.to_f) * 100 + + if reflections.present? + self.weekly_rating = reflections.map(&:rating).reduce(0, :+) / reflections.count + else + self.weekly_rating = 0 + end + end + + def arrow_and_class(sym) + begin + if send(sym) > previous_weekly_summary.send(sym) + ["▲", "better"] + elsif send(sym) < previous_weekly_summary.send(sym) + ["▼", "worse"] + else + ["−", ""] + end + rescue + ["−", ""] + end + end + + def recommendation + if completion_percentage < 70 && weekly_rating < 7 + "It looks like you didn't get everything done this past week and that reflected on how productive you thought your days were. Try being more deliberate in planning your tasks this coming week in order to build momentum." + elsif completion_percentage >= 70 && weekly_rating < 7 + "You did well completing your tasks this week, but felt you could have been more productive. You only have five tasks to plan in Volition for a day. Make sure these tasks are the ones that will make you feel like you're making the most progress in your work and life." + elsif completion_percentage < 70 && weekly_rating >= 7 + "It seems like you felt productive this week, but you didn't complete all the tasks you planned to. Try to break your tasks into smaller pieces this upcoming week." + elsif completion_percentage >= 70 && weekly_rating >= 7 + "Great job! You made a solid plan and you stuck to it. Keep up the good work!" + end + end +end diff --git a/app/serializers/data_struct_serializer.rb b/app/serializers/data_struct_serializer.rb new file mode 100644 index 0000000..f502dec --- /dev/null +++ b/app/serializers/data_struct_serializer.rb @@ -0,0 +1,10 @@ +class DataStructSerializer + def self.dump(hash) + hash.to_json + end + + def self.load(hash) + return unless hash + JSON.parse(hash, object_class: DataStruct) + end +end diff --git a/app/services/payment_service.rb b/app/services/payment_service.rb deleted file mode 100644 index b6d4312..0000000 --- a/app/services/payment_service.rb +++ /dev/null @@ -1,22 +0,0 @@ -module PaymentService - def self.charge_card(token:, user:) - begin - charge = Stripe::Charge.create( - amount: 1200, - currency: "usd", - source: token, - description: "Full access, forever.", - receipt_email: user.email - ) - if charge.status == "succeeded" - user.skip_password_validation = true - user.update!( - paid: true, - stripe_charge_id: charge.id - ) - end - rescue Stripe::CardError - false - end - end -end diff --git a/app/views/dashboard/show.html.erb b/app/views/dashboard/show.html.erb index 6a076e5..73a4e9b 100644 --- a/app/views/dashboard/show.html.erb +++ b/app/views/dashboard/show.html.erb @@ -1,7 +1,7 @@

Dashboard

<% if today_is_trackable? %> - <%= link_to "go to today's tasks →".html_safe, today_path, class: "todayLink" %> + <%= link_to "Go to today's tasks".html_safe, today_path, class: "todayLink" %> <% else %>

Have a good <%= Date::DAYNAMES[Date.current.wday] %> and get some rest.

<% end %> @@ -19,13 +19,13 @@
    <% @tomorrows_todo_list.todos.each do |todo| %>
  1. - <% if todo.complete? %> + <% if todo.complete %> - <%= truncate(todo.content, user_agent: @user_agent) %> + <%= todo.content %> <% else %> - <%= truncate(todo.content, user_agent: @user_agent) %> + <%= todo.content %> <% end %>
  2. <% end %> @@ -50,13 +50,13 @@
      <% @todays_todo_list.todos.each do |todo| %>
    1. - <% if todo.complete? %> + <% if todo.complete %> - <%= truncate(todo.content, user_agent: @user_agent) %> + <%= todo.content %> <% else %> - <%= truncate(todo.content, user_agent: @user_agent) %> + <%= todo.content %> <% end %> <% if todo.content.present? %> @@ -78,27 +78,27 @@ <% end %> <% if @past_todo_lists.present? %> - <% @past_todo_lists.each do |todo_list| %> - <%= link_to day_path(todo_list), class: 'noTextStyle' do %> + <% @past_todo_lists.each do |snapshot| %> + <%= link_to day_path(snapshot.date), class: 'noTextStyle' do %>
    2. -
      <%= todo_list.date.strftime('%B') %>
      -
      <%= todo_list.date.strftime('%e') %>
      +
      <%= snapshot.date.strftime('%B') %>
      +
      <%= snapshot.date.strftime('%e') %>
      - <%= todo_list.reflection.try(:rating) || "?" %> + <%= snapshot.reflection.try(:rating) || "?" %>
        - <% todo_list.todos.each do |todo| %> + <% snapshot.todos.each do |todo| %>
      1. - <% if todo.complete? %> + <% if todo.complete %> - <%= truncate(todo.content, user_agent: @user_agent) %> + <%= todo.content %> <% else %> - <%= truncate(todo.content, user_agent: @user_agent) %> + <%= todo.content %> <% end %> <% if todo.content.present? %> diff --git a/app/views/days/show.html.erb b/app/views/days/show.html.erb index ca9dbac..aa94d1e 100644 --- a/app/views/days/show.html.erb +++ b/app/views/days/show.html.erb @@ -1,24 +1,24 @@
        -

        <%= @todo_list.date.strftime('%B %e') %>

        +

        <%= @daily_snapshot.date.strftime('%B %e') %>

        -
        <%= @todo_list.date.strftime('%B') %>
        -
        <%= @todo_list.date.strftime('%e') %>
        +
        <%= @daily_snapshot.date.strftime('%B') %>
        +
        <%= @daily_snapshot.date.strftime('%e') %>
          - <% @todo_list.todos.each do |todo| %> + <% @daily_snapshot.todos.each do |todo| %>
        1. - <% if todo.complete? %> + <% if todo.complete %> - <%= todo.content.truncate(40) %> + <%= todo.content %> <% else %> - <%= todo.content.truncate(40) %> + <%= todo.content %> <% end %> <% if todo.content.present? %> @@ -36,5 +36,8 @@ locals: { reflection: @reflection } %> <% else %>

          No reflection for this day.

          + <% if @daily_snapshot.date == Date.yesterday %> + <%= link_to "Reflect", reflect_path(@daily_snapshot.date), class: "button" %> + <% end %> <% end %>
        diff --git a/app/views/gifts/new.html.erb b/app/views/gifts/new.html.erb new file mode 100644 index 0000000..b403e00 --- /dev/null +++ b/app/views/gifts/new.html.erb @@ -0,0 +1,42 @@ +
        +

        🎁 Give the gift of Volition 🎁

        + +

        + +

        + + <%= form_with model: @gift, local: true, id: "payment-form" do |f| %> + <% if @gift.errors.any? %> +

        You must fill out all fields.

        + <% end %> + +
        + <%= f.label :recipient_name, "Their name" %> + <%= f.text_field :recipient_name %> +
        + +
        + <%= f.label :recipient_email, "Their email" %> + <%= f.text_field :recipient_email %> +
        + +
        + <%= f.label :message %> + <%= f.text_area :message %> +
        + +
        +
        + +
        +
        + + +
        + <% end %> +
        diff --git a/app/views/gifts_mailer/gift_notification.text.erb b/app/views/gifts_mailer/gift_notification.text.erb new file mode 100644 index 0000000..159a7a1 --- /dev/null +++ b/app/views/gifts_mailer/gift_notification.text.erb @@ -0,0 +1,9 @@ +Hi, <%= @gift.recipient_name %>! + +<%= @sender.name %> has sent you a 1-year gift subscription to Volition! They sent the following message with this gift: + +<%= @gift.message %> + +To learn more about Volition, visit <%= root_url %>. + +To sign up and activate your subscription, visit <%= new_user_url(gift_token: @gift.unique_token) %> diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 2584b98..fb80fab 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -1,6 +1,6 @@