The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.
109 |├── app ├── mailers │ ├── .keep │ ├── welcome_mailer.rb │ └── prompt_mailer.rb ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── cancellation.rb │ ├── application_record.rb │ ├── import.rb │ ├── prompt_task.rb │ ├── search.rb │ ├── prompt_entry.rb │ ├── export.rb │ ├── subscription.rb │ ├── reply_token.rb │ ├── entry.rb │ ├── prompt_delivery_hour.rb │ ├── email_processor.rb │ ├── admin_dashboard.rb │ ├── user.rb │ └── ohlife_importer.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── logo.png │ │ ├── team.jpg │ │ ├── favicon.ico │ │ ├── logo-stripe.png │ │ ├── apple-touch-icon-57.png │ │ ├── apple-touch-icon-72.png │ │ ├── apple-touch-icon-114.png │ │ └── apple-touch-icon-144.png │ ├── stylesheets │ │ ├── application.sass │ │ ├── base.sass │ │ ├── base │ │ │ ├── _form.scss │ │ │ ├── _variables.scss │ │ │ ├── _footer.scss │ │ │ ├── _header.scss │ │ │ └── _body.scss │ │ └── icons.scss │ ├── javascripts │ │ ├── tooltips.js │ │ ├── application.js │ │ └── placeholders.min.js │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── config │ │ └── manifest.js ├── views │ ├── pages │ │ ├── .keep │ │ ├── github.html.erb │ │ ├── privacy.html.erb │ │ ├── ohlife-alternative.html.erb │ │ ├── faq.html.erb │ │ └── keeping-a-development-journal.html.erb │ ├── subscriptions │ │ ├── create.html.erb │ │ ├── _pricing.html.erb │ │ ├── _sign_up_link_javascript.html.erb │ │ ├── _payment_form.html.erb │ │ ├── _stripe_javascript.html.erb │ │ └── _close_account.html.erb │ ├── entries │ │ ├── index.html.erb │ │ ├── _date.html.erb │ │ ├── _entry.html.erb │ │ ├── edit.html.erb │ │ └── _welcome.html.erb │ ├── kaminari │ │ ├── _paginator.html.erb │ │ ├── _first_page.html.erb │ │ ├── _next_page.html.erb │ │ └── _prev_page.html.erb │ ├── layouts │ │ ├── _footer.html.erb │ │ ├── _default_content.html.erb │ │ ├── _apple_touch_icons.html.erb │ │ ├── skinny.html.erb │ │ └── application.html.erb │ ├── devise │ │ ├── mailer │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── unlock_instructions.html.erb │ │ │ └── reset_password_instructions.html.erb │ │ ├── unlocks │ │ │ └── new.html.erb │ │ ├── confirmations │ │ │ └── new.html.erb │ │ ├── passwords │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── sessions │ │ │ └── new.html.erb │ │ └── shared │ │ │ └── _links.erb │ ├── searches │ │ ├── _entry.html.erb │ │ └── show.html.erb │ ├── application │ │ ├── _flashes.html.erb │ │ ├── _javascript.html.erb │ │ ├── _analytics.html.erb │ │ └── _navbar.html.erb │ ├── landing │ │ ├── _header.html.erb │ │ └── show.html.erb │ ├── welcome_mailer │ │ └── welcome.html.erb │ ├── prompt_mailer │ │ └── prompt.html.erb │ ├── imports │ │ └── new.html.erb │ ├── settings │ │ └── edit.html.erb │ ├── admin_dashboard │ │ └── show.html.erb │ └── credit_cards │ │ ├── edit.html.erb │ │ └── _stripe_javascript.html.erb ├── controllers │ ├── concerns │ │ └── .keep │ ├── landing_controller.rb │ ├── exports_controller.rb │ ├── application_controller.rb │ ├── searches_controller.rb │ ├── admin_dashboard_controller.rb │ ├── settings_controller.rb │ ├── credit_cards_controller.rb │ ├── entries_controller.rb │ ├── cancellations_controller.rb │ ├── imports_controller.rb │ └── subscriptions_controller.rb ├── helpers │ └── application_helper.rb ├── uploaders │ ├── ohlife_export_uploader.rb │ └── photo_uploader.rb ├── workers │ ├── welcome_mailer_worker.rb │ ├── prompt_worker.rb │ └── ohlife_import_worker.rb └── transitions │ ├── fix_future_dated_entries_transition.rb │ └── backfill_subscriptions_transition.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ ├── trailmix.rake │ └── development_seeds.rake └── templates │ └── erb │ └── scaffold │ └── _form.html.erb ├── public ├── favicon.ico ├── robots.txt └── 404.html ├── spec ├── helpers │ └── .keep ├── lib │ └── .keep ├── controllers │ ├── .keep │ ├── credit_cards_controller_spec.rb │ ├── exports_controller_spec.rb │ ├── searches_controller_spec.rb │ ├── settings_controller_spec.rb │ ├── landing_controller_spec.rb │ ├── imports_controller_spec.rb │ ├── admin_dashboard_controller_spec.rb │ ├── cancellations_controller_spec.rb │ └── subscriptions_controller_spec.rb ├── features │ ├── .keep │ ├── user_signs_out_spec.rb │ ├── user_exports_entries_spec.rb │ ├── user_searches_entries_spec.rb │ ├── server_sends_daily_email_prompt_spec.rb │ ├── user_imports_ohlife_entries_spec.rb │ ├── user_cancels_account_spec.rb │ ├── user_edits_entry_spec.rb │ ├── user_updates_credit_card_spec.rb │ ├── user_changes_settings_spec.rb │ ├── user_views_entries_spec.rb │ └── user_responds_to_prompt_spec.rb ├── support │ ├── features │ │ ├── .keep │ │ └── warden_helpers.rb │ ├── matchers │ │ └── .keep │ ├── mixins │ │ └── .keep │ ├── shared_examples │ │ └── .keep │ ├── factory_bot.rb │ ├── action_mailer.rb │ ├── carrierwave.rb │ ├── sidekiq.rb │ └── database_cleaner.rb ├── fixtures │ └── files │ │ ├── photo.jpg │ │ └── ohlife_export.txt ├── models │ ├── prompt_entry_spec.rb │ ├── prompt_task_spec.rb │ ├── reply_token_spec.rb │ ├── export_spec.rb │ ├── ohlife_importer_spec.rb │ ├── search_spec.rb │ ├── entry_spec.rb │ ├── prompt_delivery_hour_spec.rb │ ├── user_spec.rb │ ├── admin_dashboard_spec.rb │ └── email_processor_spec.rb ├── mailers │ ├── welcome_mailer_spec.rb │ └── prompt_mailer_spec.rb ├── views │ └── searches │ │ ├── _entry.html.erb_spec.rb │ │ └── show.html.erb_spec.rb ├── tasks │ └── trailmix_spec.rb ├── factories.rb ├── spec_helper.rb └── transitions │ ├── fix_future_dated_entries_transition_spec.rb │ └── backfill_subscriptions_transition_spec.rb ├── .ruby-version ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── .rspec ├── circle.yml ├── bin ├── dev ├── rake ├── bundle ├── rails ├── rspec ├── delayed_job ├── spring ├── yarn ├── update └── setup ├── Procfile ├── config ├── initializers │ ├── kaminari_config.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── redis.rb │ ├── chartkick.rb │ ├── yaml.rb │ ├── stripe.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── griddler.rb │ ├── filter_parameter_logging.rb │ ├── permissions_policy.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── errors.rb │ ├── carrierwave.rb │ ├── sidekiq.rb │ ├── cve_patches.rb │ ├── content_security_policy.rb │ └── airbrake.rb ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── i18n-tasks.yml ├── secrets.yml ├── database.yml ├── smtp.rb ├── environments │ ├── staging.rb │ ├── test.rb │ ├── development.rb │ └── production.rb ├── locales │ ├── en.yml │ ├── simple_form.en.yml │ └── devise.en.yml ├── puma.rb ├── routes.rb ├── storage.yml └── application.rb ├── db ├── migrate │ ├── 20140923002749_drop_delayed_jobs.rb │ ├── 20141002193215_add_date_to_entry.rb │ ├── 20141010010843_add_stripe_id_to_user.rb │ ├── 20160125013209_add_photos_to_entries.rb │ ├── 20140925183528_add_import_id_to_entry.rb │ ├── 20141004031950_add_reply_token_to_user.rb │ ├── 20140924213544_remove_stripe_customer_id_from_user.rb │ ├── 20140926025305_add_raw_file_to_import.rb │ ├── 20140924172056_add_stripe_customer_id_to_user.rb │ ├── 20141010201317_remove_stripe_id_from_user.rb │ ├── 20140922202440_create_entries.rb │ ├── 20141004062429_add_index_and_not_null_to_user_reply_token.rb │ ├── 20141004203140_add_not_null_to_entry_date.rb │ ├── 20140927193748_remove_default_ohlife_export.rb │ ├── 20140925173215_create_imports.rb │ ├── 20140928015646_add_time_zone_to_user.rb │ ├── 20140928181424_add_prompt_delivery_hour_to_users.rb │ ├── 20141011191307_create_cancellation.rb │ ├── 20140926024949_remove_paperclip_from_import.rb │ ├── 20140924180559_add_not_null_to_user_timestamps.rb │ ├── 20140924180920_add_not_null_to_entry_timestamps.rb │ ├── 20141010200536_create_subscription.rb │ ├── 20140921195949_create_delayed_jobs.rb │ └── 20140921203919_devise_create_users.rb ├── seeds.rb └── schema.rb ├── .gitignore ├── config.ru ├── Rakefile ├── .sample.env ├── .travis.yml ├── .github └── workflows │ └── rubyonrails.yml ├── LICENSE ├── Gemfile └── README.md /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/lib/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.2 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/pages/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/features/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/support/features/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/support/matchers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/support/mixins/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/support/shared_examples/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /app/views/subscriptions/create.html.erb: -------------------------------------------------------------------------------- 1 |
Success!
2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | checkout: 2 | post: 3 | - cp .sample.env .env 4 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /app/models/cancellation.rb: -------------------------------------------------------------------------------- 1 | class Cancellation < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb 2 | worker: bundle exec sidekiq 3 | -------------------------------------------------------------------------------- /app/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/logo.png -------------------------------------------------------------------------------- /app/assets/images/team.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/team.jpg -------------------------------------------------------------------------------- /app/assets/stylesheets/application.sass: -------------------------------------------------------------------------------- 1 | @import "bootstrap-theme" 2 | @import "base" 3 | @import "icons" 4 | -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /spec/fixtures/files/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/spec/fixtures/files/photo.jpg -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | Kaminari.configure do |config| 2 | config.default_per_page = 1 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/logo-stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/logo-stripe.png -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/assets/images/apple-touch-icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/apple-touch-icon-57.png -------------------------------------------------------------------------------- /app/assets/images/apple-touch-icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/apple-touch-icon-72.png -------------------------------------------------------------------------------- /app/assets/javascripts/tooltips.js: -------------------------------------------------------------------------------- 1 | // Enable any tooltips on the page 2 | $(function () { $("[data-toggle='tooltip']").tooltip(); }); 3 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /app/assets/images/apple-touch-icon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/apple-touch-icon-114.png -------------------------------------------------------------------------------- /app/assets/images/apple-touch-icon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/images/apple-touch-icon-144.png -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /spec/support/action_mailer.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each) do 3 | ActionMailer::Base.deliveries.clear 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecation/trailmix/HEAD/app/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base.sass: -------------------------------------------------------------------------------- 1 | @import "base/_variables" 2 | @import "base/_body" 3 | @import "base/_form" 4 | @import "base/_header" 5 | @import "base/_footer" 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /db/migrate/20140923002749_drop_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class DropDelayedJobs < ActiveRecord::Migration 2 | def change 3 | drop_table :delayed_jobs 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/models/import.rb: -------------------------------------------------------------------------------- 1 | class Import < ApplicationRecord 2 | has_many :entries 3 | belongs_to :user 4 | 5 | mount_uploader :ohlife_export, OhlifeExportUploader 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20141002193215_add_date_to_entry.rb: -------------------------------------------------------------------------------- 1 | class AddDateToEntry < ActiveRecord::Migration 2 | def change 3 | add_column :entries, :date, :date 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/entries/index.html.erb: -------------------------------------------------------------------------------- 1 | <% if @entries.empty? %> 2 | <%= render "welcome" %> 3 | <% else %> 4 | <%= render partial: "entry", collection: @entries %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_trailmix_session' 4 | -------------------------------------------------------------------------------- /db/migrate/20141010010843_add_stripe_id_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddStripeIdToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :stripe_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160125013209_add_photos_to_entries.rb: -------------------------------------------------------------------------------- 1 | class AddPhotosToEntries < ActiveRecord::Migration 2 | def change 3 | add_column :entries, :photo, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.keep 2 | *.DS_Store 3 | *.swo 4 | *.swp 5 | /.bundle 6 | /.env 7 | /.foreman 8 | /coverage/* 9 | /db/*.sqlite3 10 | /log/* 11 | /public/system 12 | /tags 13 | /tmp/* 14 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link favicon.ico 2 | //= link chartkick.js 3 | //= link_directory ../javascripts .js 4 | //= link_directory ../stylesheets .css 5 | //= link_tree ../images 6 | -------------------------------------------------------------------------------- /app/controllers/landing_controller.rb: -------------------------------------------------------------------------------- 1 | class LandingController < ApplicationController 2 | def show 3 | if signed_in? 4 | redirect_to entries_path 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/uploaders/ohlife_export_uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class OhlifeExportUploader < CarrierWave::Uploader::Base 4 | def extension_white_list 5 | %w(txt) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /db/migrate/20140925183528_add_import_id_to_entry.rb: -------------------------------------------------------------------------------- 1 | class AddImportIdToEntry < ActiveRecord::Migration 2 | def change 3 | add_column :entries, :import_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141004031950_add_reply_token_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddReplyTokenToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :reply_token, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require 'bundler/setup' 7 | load Gem.bin_path('rspec-core', 'rspec') 8 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.erb: -------------------------------------------------------------------------------- 1 | <%= paginator.render do -%> 2 |9 | <%= l(date, format: :month_day_year) %> 10 |
11 | -------------------------------------------------------------------------------- /app/views/layouts/_default_content.html.erb: -------------------------------------------------------------------------------- 1 | 2 |Welcome <%= @email %>!
2 | 3 |You can confirm your account email through the link below:
4 | 5 |<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
6 | -------------------------------------------------------------------------------- /app/controllers/exports_controller.rb: -------------------------------------------------------------------------------- 1 | class ExportsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def new 5 | export = Export.new(current_user) 6 | 7 | send_data export.to_json, filename: export.filename 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/searches/_entry.html.erb: -------------------------------------------------------------------------------- 1 |A private place to write.
4 |5 | <%= link_to "Get started", 6 | "#sign-up-form", 7 | class: "btn btn-default btn-lg sign-up-link" %> 8 |
9 |Hello <%= @resource.email %>!
2 | 3 |Your account has been locked due to an excessive number of unsuccessful sign in attempts.
4 | 5 |Click the link below to unlock your account:
6 | 7 |<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
8 | -------------------------------------------------------------------------------- /db/migrate/20141011191307_create_cancellation.rb: -------------------------------------------------------------------------------- 1 | class CreateCancellation < ActiveRecord::Migration 2 | def change 3 | create_table :cancellations do |t| 4 | t.string :email, null: false 5 | t.string :stripe_customer_id, null: false 6 | t.text :reason 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140926024949_remove_paperclip_from_import.rb: -------------------------------------------------------------------------------- 1 | class RemovePaperclipFromImport < ActiveRecord::Migration 2 | def change 3 | remove_column :imports, :raw_file_file_name 4 | remove_column :imports, :raw_file_content_type 5 | remove_column :imports, :raw_file_file_size 6 | remove_column :imports, :raw_file_updated_at 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/layouts/skinny.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :content do %> 2 | 3 |9 | <%= link_to "Edit this entry", edit_entry_path(entry) %> 10 |
11 | 12 | <%= paginate @entries %> 13 | 14 | <%= simple_format(entry.body) %> 15 | 16 | <% if entry.photo.url %> 17 | <%= image_tag entry.photo.url, width: 400 %> 18 | <% end %> 19 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |Hello <%= @resource.email %>!
2 | 3 |Someone has requested a link to change your password. You can do this through the link below.
4 | 5 |<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
6 | 7 |If you didn't request this, please ignore this email.
8 |Your password won't change until you access the link above and create a new one.
9 | -------------------------------------------------------------------------------- /app/models/reply_token.rb: -------------------------------------------------------------------------------- 1 | class ReplyToken 2 | def initialize(email) 3 | @email = email 4 | end 5 | 6 | def self.generate(email) 7 | new(email).generate 8 | end 9 | 10 | def generate 11 | "#{username}.#{random_suffix}".downcase 12 | end 13 | 14 | private 15 | 16 | attr_reader :email 17 | 18 | def random_suffix 19 | SecureRandom.urlsafe_base64(8) 20 | end 21 | 22 | def username 23 | email.split("@").first 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/mailers/welcome_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | describe WelcomeMailer do 2 | describe ".welcome" do 3 | it "welcomes the user" do 4 | user = create(:user) 5 | 6 | mail = WelcomeMailer.welcome(user) 7 | 8 | expect(mail.body.encoded).to include("Welcome to Trailmix") 9 | expect(mail.subject).to include("Write your first entry") 10 | expect(mail.from).to eq([user.reply_email]) 11 | expect(mail.to).to eq([user.email]) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/entry.rb: -------------------------------------------------------------------------------- 1 | class Entry < ApplicationRecord 2 | belongs_to :import, optional: true 3 | belongs_to :user 4 | 5 | mount_uploader :photo, PhotoUploader 6 | 7 | def self.by_date 8 | order("date DESC") 9 | end 10 | 11 | def self.newest 12 | by_date.first 13 | end 14 | 15 | def self.random 16 | order(Arel.sql("RANDOM()")).first 17 | end 18 | 19 | def for_today? 20 | date == Time.zone.now.in_time_zone(user.time_zone).to_date 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/admin_dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class AdminDashboardController < ApplicationController 2 | before_action :authenticate_user!, :restrict_to_admins 3 | 4 | def show 5 | @dashboard = AdminDashboard.new 6 | end 7 | 8 | private 9 | 10 | def restrict_to_admins 11 | unless admin_emails.include?(current_user.email) 12 | redirect_to entries_path 13 | end 14 | end 15 | 16 | def admin_emails 17 | ENV.fetch("ADMIN_EMAILS").split(",") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/welcome_mailer/welcome.html.erb: -------------------------------------------------------------------------------- 1 |<%= WelcomeMailer::START_OF_MESSAGE %>
2 | 3 |To create your first entry, simply reply to this email. That's it.
4 | 5 |If you like, you can even attach a photo!
6 | 7 |We'll email you once a day to remind you to keep up the habit. We'll include a previous entry each time (they're super fun to read).
8 | 9 |Don't worry if you need to skip a day. You can just ignore or delete our email and nothing bad will happen.
10 | -------------------------------------------------------------------------------- /spec/support/sidekiq.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/testing' 2 | 3 | RSpec.configure do |config| 4 | config.before(:each) do |example| 5 | Sidekiq::Worker.clear_all 6 | 7 | if example.metadata[:sidekiq] == :fake 8 | Sidekiq::Testing.fake! 9 | elsif example.metadata[:sidekiq] == :inline 10 | Sidekiq::Testing.inline! 11 | elsif example.metadata[:type] == :acceptance 12 | Sidekiq::Testing.inline! 13 | else 14 | Sidekiq::Testing.fake! 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/settings_controller.rb: -------------------------------------------------------------------------------- 1 | class SettingsController < ApplicationController 2 | layout "skinny" 3 | 4 | before_action :authenticate_user! 5 | 6 | def edit 7 | end 8 | 9 | def update 10 | current_user.update!(user_params) 11 | 12 | flash[:notice] = "Your settings have been saved." 13 | 14 | redirect_to entries_path 15 | end 16 | 17 | private 18 | 19 | def user_params 20 | params.require(:user).permit(:time_zone, :prompt_delivery_hour) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/views/searches/_entry.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | describe "searches/_entry" do 2 | it "highlights the searched-for term" do 3 | user = build_stubbed(:user) 4 | allow(view).to receive(:current_user).and_return(user) 5 | entry = build_stubbed(:entry, body: "I want to get a cat") 6 | search = double("search", term: "cat") 7 | assign(:search, search) 8 | 9 | render "searches/entry", entry: entry 10 | 11 | expect(rendered).to have_css("mark", text: search.term) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/credit_cards_controller.rb: -------------------------------------------------------------------------------- 1 | class CreditCardsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | layout "skinny" 5 | 6 | def edit 7 | end 8 | 9 | def update 10 | stripe_customer = 11 | Stripe::Customer.retrieve(current_user.stripe_customer_id) 12 | stripe_customer.card = params[:stripeToken] 13 | stripe_customer.save 14 | 15 | flash.notice = "Credit card updated successfully" 16 | redirect_to action: :edit 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/models/prompt_task_spec.rb: -------------------------------------------------------------------------------- 1 | describe PromptTask do 2 | describe "#run" do 3 | it "enqueues a job for each user" do 4 | user = double("user", id: 1) 5 | other_user = double("other_user", id: 2) 6 | worker = double("worker", perform_async: nil) 7 | 8 | PromptTask.new([1, 2], worker).run 9 | 10 | expect(worker).to have_received(:perform_async).with(user.id) 11 | expect(worker).to have_received(:perform_async).with(other_user.id) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:suite) do 3 | DatabaseCleaner.clean_with(:deletion) 4 | end 5 | 6 | config.before(:each) do 7 | DatabaseCleaner.strategy = :transaction 8 | end 9 | 10 | config.before(:each, :js => true) do 11 | DatabaseCleaner.strategy = :deletion 12 | end 13 | 14 | config.before(:each) do 15 | DatabaseCleaner.start 16 | end 17 | 18 | config.after(:each) do 19 | DatabaseCleaner.clean 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/entries/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header do %> 2 | <%= render partial: "date", 3 | object: @entry.date, 4 | locals: { today: @entry.for_today? } 5 | %> 6 | <% end %> 7 | 8 |9 | <%= form_for @entry do |f| %> 10 |
<%= PromptMailer::PROMPT_TEXT %>
2 | 3 |Simply reply to this email with your entry.
4 | 5 | <% if @announcement.present? %> 6 | <%= @announcement.html_safe %> 7 | <% end %> 8 | 9 | <% if @entry %> 10 |Remember this? On <%= I18n.l(@entry.date, format: :month_day_year) %> (<%= (distance_of_time_in_words(@date, @entry.date)) %> ago), you wrote the following:
11 | 12 |----------------------
13 | 14 | <%= simple_format @entry.body %> 15 | 16 | <% if @entry.photo.url %> 17 | <%= image_tag @entry.photo.url, width: 400 %> 18 | <% end %> 19 | <% end %> 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - "echo '--colour' > ~/.rspec" 3 | - "echo 'gem: --no-document' > ~/.gemrc" 4 | - "gem install bundler --version '1.17.3'" 5 | - export DISPLAY=:99.0 6 | before_script: 7 | - cp .sample.env .env 8 | - psql -c 'create database "trailmix_test";' -U postgres 9 | - bundle exec rake db:setup 10 | addons: 11 | postgresql: 9.6 12 | services: 13 | - xvfb 14 | - postgresql 15 | dist: trusty 16 | branches: 17 | only: 18 | - master 19 | cache: 20 | - bundler 21 | language: 22 | - ruby 23 | notifications: 24 | email: 25 | - false 26 | rvm: 27 | - 2.5.7 28 | addons: 29 | postgresql: "9.3" 30 | -------------------------------------------------------------------------------- /app/controllers/cancellations_controller.rb: -------------------------------------------------------------------------------- 1 | class CancellationsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def create 5 | create_cancellation 6 | current_user.destroy! 7 | flash[:notice] = "Your account has been removed and " + 8 | "your subscription has been canceled." 9 | 10 | redirect_to new_registration_path 11 | end 12 | 13 | private 14 | 15 | def create_cancellation 16 | Cancellation.create!(email: current_user.email, 17 | stripe_customer_id: current_user.stripe_customer_id, 18 | reason: params[:reason]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/transitions/backfill_subscriptions_transition.rb: -------------------------------------------------------------------------------- 1 | class BackfillSubscriptionsTransition 2 | def self.perform 3 | new.perform 4 | end 5 | 6 | def perform 7 | stripe_customers.each do |stripe_customer| 8 | create_subscription_for(stripe_customer) 9 | end 10 | end 11 | 12 | private 13 | 14 | def create_subscription_for(stripe_customer) 15 | user = User.find_by(email: stripe_customer.email) 16 | 17 | if user && user.subscription.blank? 18 | user.create_subscription!(stripe_customer_id: stripe_customer.id) 19 | end 20 | end 21 | 22 | def stripe_customers 23 | Stripe::Customer.all(limit: 100) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header do %> 2 |3 |
We'll email you reset instructions.
4 | <% end %> 5 | 6 | <%= simple_form_for( 7 | resource, 8 | as: resource_name, 9 | url: password_path(resource_name), 10 | html: { method: :post } 11 | ) do |f| %> 12 | 13 | <%= f.error_notification %> 14 | 15 |A private place to write.
5 |9 | Hey there person referred from GitHub! 10 |
11 | 12 |13 | This is Trailmix, a service that helps you build a journaling habit that will 14 | actually stick. 15 |
16 | 17 |18 | As a developer or designer, you might enjoy <%= link_to "4 Reasons to Keep a 19 | Development Journal", page_path("keeping-a-development-journal") %>. 20 |
21 | 22 |23 | Or, if you prefer, you can simply 24 | <%= link_to "head to our standard landing page", new_registration_path %>. 25 |
26 | -------------------------------------------------------------------------------- /config/initializers/errors.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require "net/smtp" 3 | 4 | # Example: 5 | # begin 6 | # some http call 7 | # rescue *HTTP_ERRORS => error 8 | # notify_hoptoad error 9 | # end 10 | 11 | HTTP_ERRORS = [ 12 | EOFError, 13 | Errno::ECONNRESET, 14 | Errno::EINVAL, 15 | Net::HTTPBadResponse, 16 | Net::HTTPHeaderSyntaxError, 17 | Net::ProtocolError, 18 | Timeout::Error 19 | ] 20 | 21 | SMTP_SERVER_ERRORS = [ 22 | IOError, 23 | Net::SMTPAuthenticationError, 24 | Net::SMTPServerBusy, 25 | Net::SMTPUnknownError, 26 | Timeout::Error 27 | ] 28 | 29 | SMTP_CLIENT_ERRORS = [ 30 | Net::SMTPFatalError, 31 | Net::SMTPSyntaxError 32 | ] 33 | 34 | SMTP_ERRORS = SMTP_SERVER_ERRORS + SMTP_CLIENT_ERRORS 35 | -------------------------------------------------------------------------------- /spec/views/searches/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | describe "searches/show" do 2 | context "when no entries are found" do 3 | it "informs the user" do 4 | search = mock_model("Search", term: "cat", entries: []) 5 | assign(:search, search) 6 | 7 | render template: "searches/show" 8 | 9 | expect(rendered).to(include("Sorry, we couldn't find any entries")) 10 | end 11 | end 12 | 13 | context "when no search term is provided" do 14 | it "does not inform the user" do 15 | search = mock_model("Search", term: "", entries: []) 16 | assign(:search, search) 17 | 18 | render template: "searches/show" 19 | 20 | expect(rendered).to_not include("we couldn't find any entries") 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require bootstrap.min 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /config/initializers/carrierwave.rb: -------------------------------------------------------------------------------- 1 | CarrierWave.configure do |config| 2 | config.storage = :fog 3 | 4 | config.fog_credentials = { 5 | provider: "AWS", 6 | aws_access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"), 7 | aws_secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY") 8 | } 9 | 10 | config.fog_directory = "trailmix" 11 | config.fog_public = false 12 | end 13 | 14 | module CarrierWave 15 | module MiniMagick 16 | def quality(percentage) 17 | manipulate! do |image| 18 | image.quality(percentage) 19 | image 20 | end 21 | end 22 | 23 | def auto_orient 24 | manipulate! do |image| 25 | image.auto_orient 26 | image 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | require "sidekiq" 2 | require "sidekiq/web" 3 | 4 | Sidekiq::Web.use(Rack::Auth::Basic) do |user, password| 5 | [user, password] == [ 6 | ENV.fetch("SIDEKIQ_WEB_USER"), 7 | ENV.fetch("SIDEKIQ_WEB_PASSWORD") 8 | ] 9 | end 10 | 11 | # https://devcenter.heroku.com/articles/connecting-heroku-redis#connecting-in-ruby 12 | Sidekiq.configure_server do |config| 13 | config.redis = { 14 | url: ENV["REDIS_URL"], 15 | ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } 16 | } 17 | end 18 | 19 | Sidekiq.configure_client do |config| 20 | config.redis = { 21 | url: ENV["REDIS_URL"], 22 | ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } 23 | } 24 | config.logger = Rails.logger if Rails.env.test? 25 | end 26 | -------------------------------------------------------------------------------- /app/mailers/prompt_mailer.rb: -------------------------------------------------------------------------------- 1 | class PromptMailer < ActionMailer::Base 2 | PROMPT_TEXT = "How was your day?" 3 | 4 | def prompt(user, entry, date = nil) 5 | @entry = entry 6 | @date = date || Time.current.in_time_zone(user.time_zone).to_date 7 | @announcement = ENV["ANNOUNCEMENT"] 8 | 9 | mail( 10 | from: "Trailmix <#{user.reply_email}>", 11 | to: user.email, 12 | subject: Subject.new(user, @date) 13 | ) 14 | end 15 | 16 | class Subject 17 | def initialize(user, date) 18 | @user = user 19 | @date = date 20 | end 21 | 22 | def to_s 23 | "It's #{date}. How was your day?" 24 | end 25 | 26 | private 27 | 28 | def date 29 | I18n.l(@date, format: :prompt_subject_line) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/features/user_imports_ohlife_entries_spec.rb: -------------------------------------------------------------------------------- 1 | feature "User imports their OhLife entries" do 2 | scenario "immediately after creating their account", sidekiq: :inline do 3 | user = create(:user) 4 | 5 | login_as(user) 6 | visit entries_path 7 | click_link "import your OhLife entries" 8 | file_path = Rails.root + "spec/fixtures/files/ohlife_export.txt" 9 | attach_file('import_ohlife_export', file_path) 10 | click_button "Start Import" 11 | 12 | expect(page).to have_content("We're importing your entries") 13 | expect(Import.count).to eq(1) 14 | expect(user.entries.count).to eq(2) 15 | end 16 | 17 | scenario "when signed out" do 18 | visit new_import_path 19 | 20 | expect(current_path).to eq(new_user_session_path) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header do %> 2 |4 | <%= link_to "Forget your password?", new_password_path(:user) %> 5 |
6 | <% end %> 7 | 8 | <%= simple_form_for( 9 | resource, 10 | as: resource_name, 11 | url: session_path(resource_name) 12 | ) do |f| %> 13 | 14 |2 |
8 | Your entries are not publicly-accessible. They can only be viewed by logging 9 | into your account or when sent to you via email. 10 |
11 | 12 |13 | We will never view your entries unless absolutely necessary for technical 14 | reasons, such as fixing a bug (unlikely). 15 |
16 | 17 |18 | The entries are not searchable by Google. 19 |
20 | 21 |24 | If you no longer wish to use Trailmix, you can delete your account and all 25 | your entries with a click. 26 |
27 | 28 |31 | We will never sell or share your information with third parties. 32 |
33 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | devise_for :users 3 | 4 | mount_griddler 5 | 6 | require "sidekiq/web" 7 | mount Sidekiq::Web => "/jobs" 8 | 9 | get "/admin_dashboard", to: "admin_dashboard#show", as: :admin_dashboard 10 | get "/landing", to: "landing#show", as: :new_registration 11 | get "/search", to: "searches#show" 12 | 13 | get "/pages/ohlife_refugees", to: redirect("/pages/ohlife-alternative") 14 | 15 | resources :cancellations, only: [:create] 16 | resource :credit_card, only: [:edit, :update] 17 | resources :entries, only: [:index, :edit, :update] 18 | resource :export, only: [:new] 19 | resources :imports, only: [:new, :create] 20 | resource :settings, only: [:edit, :update] 21 | resources :subscriptions, only: [:create] 22 | 23 | root to: "landing#show" 24 | end 25 | -------------------------------------------------------------------------------- /spec/features/user_cancels_account_spec.rb: -------------------------------------------------------------------------------- 1 | feature "User cancels account" do 2 | let(:stripe_helper) { StripeMock.create_test_helper } 3 | before { StripeMock.start } 4 | after { StripeMock.stop } 5 | 6 | scenario "and their information is removed" do 7 | stripe_customer = Stripe::Customer.create 8 | subscription = create(:subscription, stripe_customer_id: stripe_customer.id) 9 | user = subscription.user 10 | entry = create(:entry, user: user) 11 | 12 | login_as(user) 13 | visit edit_settings_path 14 | click_link "Close my account" 15 | fill_in :reason, with: "I decided I hate journaling." 16 | click_button "Close my account" 17 | 18 | expect(page).to have_content("account has been removed") 19 | expect(page).to have_content("subscription has been canceled") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/features/user_edits_entry_spec.rb: -------------------------------------------------------------------------------- 1 | feature "User edits entry" do 2 | scenario "and the entry body is updated" do 3 | user = create(:user) 4 | create(:entry, user: user, body: "Original body") 5 | 6 | login_as(user) 7 | visit entries_path 8 | click_link "Edit this entry" 9 | fill_in :entry_body, with: "New body" 10 | click_button "Save Entry" 11 | 12 | expect(page).to_not have_content("Original body") 13 | expect(page).to have_content("New body") 14 | end 15 | 16 | scenario "and the entry belongs to another user" do 17 | user = create(:user) 18 | another_user = create(:user) 19 | another_users_entry = create(:entry, user: another_user) 20 | 21 | login_as(user) 22 | 23 | visit edit_entry_path(another_users_entry) 24 | expect(page.status_code).to eq(404) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Labels and hints examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | 27 | -------------------------------------------------------------------------------- /app/controllers/imports_controller.rb: -------------------------------------------------------------------------------- 1 | class ImportsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def new 5 | @import = Import.new 6 | end 7 | 8 | def create 9 | import = Import.new(import_params.merge(user: current_user)) 10 | 11 | if import.save 12 | OhlifeImportWorker.perform_async(current_user.id, import.id) 13 | 14 | flash[:notice] = "We're importing your entries. Try refreshing the page "\ 15 | "in a few seconds." 16 | redirect_to entries_path 17 | else 18 | flash[:error] = "Sorry, we had trouble importing that. :( Need help? "\ 19 | "Contact us at team@trailmix.life" 20 | redirect_to new_import_path 21 | end 22 | end 23 | 24 | private 25 | 26 | def import_params 27 | params.require(:import).permit(:ohlife_export) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/features/user_updates_credit_card_spec.rb: -------------------------------------------------------------------------------- 1 | feature "User updates their credit card" do 2 | let(:stripe_helper) { StripeMock.create_test_helper } 3 | before { StripeMock.start } 4 | after { StripeMock.stop } 5 | 6 | scenario "successfully" do 7 | stripe_customer = Stripe::Customer.create 8 | user = create(:user) 9 | create(:subscription, user: user, stripe_customer_id: stripe_customer.id) 10 | 11 | login_as(user) 12 | update_credit_card 13 | 14 | expect(page).to have_content("Credit card updated successfully") 15 | end 16 | 17 | def update_credit_card 18 | visit edit_settings_path 19 | click_link "Update credit card" 20 | fill_in "number", with: "4242424242424242" 21 | fill_in "exp_month", with: "04" 22 | fill_in "exp_year", with: "2016" 23 | fill_in "cvc", with: "216" 24 | click_button "Update credit card" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/views/entries/_welcome.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header do %> 2 |6 | You're now well on your way to a journaling habit that will stick. You don't 7 | have any entries yet, but you'll be creating one soon. 8 |
9 | 10 |13 | We email you to ask how your day was and you just hit reply and start typing. 14 | Feel free to attach a photo as well! Your entries will be saved here 15 | automatically. 16 |
17 | 18 |19 | Your first email is probably already in your inbox. Try writing just a 20 | sentence or two at first. It's fine to write more, but even short entries 21 | will be interesting when you read them later. 22 |
23 | 24 |27 | You can <%= link_to "import your OhLife entries here", new_import_path %>. 28 |
29 | -------------------------------------------------------------------------------- /app/models/email_processor.rb: -------------------------------------------------------------------------------- 1 | class EmailProcessor 2 | def initialize(email) 3 | @email = email 4 | end 5 | 6 | def process 7 | entry = Entry.find_or_initialize_by(user: user, date: date) 8 | entry.update!(body: body, photo: attachment) 9 | end 10 | 11 | private 12 | 13 | attr_reader :email 14 | 15 | def body 16 | EmailReplyParser.parse_reply(email.body) 17 | end 18 | 19 | def user 20 | @user ||= User.find_by!(reply_token: reply_token) 21 | end 22 | 23 | def attachment 24 | if email.attachments 25 | email.attachments.first 26 | end 27 | end 28 | 29 | def reply_token 30 | email.to.first[:token].downcase 31 | end 32 | 33 | def date 34 | date = Date.parse(email.subject) 35 | date.future? ? (date - 1.year) : date 36 | rescue 37 | today 38 | end 39 | 40 | def today 41 | Time.zone.now.in_time_zone(user.time_zone) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/views/subscriptions/_payment_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag subscriptions_path, id: "sign-up-form" do %> 2 |3 |
8 |
10 | If you haven't exported your OhLife entries yet, do so now. (And hurry, 11 | exports won't be available after October 11th!) Export your entries by 12 | visiting the OhLife 13 | <%= link_to "export page", "https://ohlife.com/export" %>. 14 |
17 | Click "Choose File" below, and select the export you just downloaded. 18 |
21 | Click "Start Import" 22 |
We found <%= @search.entries.count %> entries.
27 | <%= render partial: "entry", collection: @search.entries %> 28 | <% elsif @search.term.present? %> 29 |30 | Sorry, we couldn't find any entries that include 31 | <%= @search.term %>. 32 |
33 | <% end %> 34 | -------------------------------------------------------------------------------- /app/views/subscriptions/_stripe_javascript.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :javascript do %> 2 | <%= javascript_include_tag "https://checkout.stripe.com/checkout.js" %> 3 | <%= javascript_tag do %> 4 | var handler = StripeCheckout.configure({ 5 | key: "<%= Rails.configuration.stripe[:publishable_key] %>", 6 | image: "<%= image_path 'logo-stripe.png' %>", 7 | name: "Trailmix", 8 | description: "Monthly subscription with trial", 9 | amount: 399, 10 | panelLabel: "{{amount}} after 14-day trial", 11 | allowRememberMe: false, 12 | token: function(token) { 13 | $("#sign-up-button").button("loading"); 14 | $("#stripe_card_id").val(token.id); 15 | $("#sign-up-form").submit(); 16 | } 17 | }); 18 | 19 | $("#sign-up-button").on("click", function(e) { 20 | e.preventDefault(); 21 | 22 | handler.open({ 23 | email: $("#email").val() 24 | }); 25 | }); 26 | <% end %> 27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/views/settings/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header do %> 2 |When would you like your daily email?
4 | <% end %> 5 | 6 |7 | <%= form_tag settings_path, method: :put do %> 8 |
31 | <%= link_to "Update credit card", edit_credit_card_path %> | 32 | <%= render "subscriptions/close_account" %> 33 |
34 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |Are we still growing?
5 | <%= line_chart @dashboard.users_by_day_since(30.days.ago) %>Are customers using the service?
9 | <%= line_chart @dashboard.entries_by_day_since(30.days.ago) %>Does anyone need help?
13 || Signed up | 17 |Entries | 18 |Per day | 19 ||
|---|---|---|---|
| <%= link_to user.email, "mailto:#{user.email}" %> | 24 |<%= l(user.created_at.to_date, format: :month_day) %> | 25 |<%= user.entries.count %> | 26 |<%= @dashboard.entries_per_day_for(user) %> | 27 |
We've got a home for you.
6 | <% end %> 7 | 8 |9 | We loved OhLife and are sad to see it go, so we built a 10 | replacement with a single additional feature: paid plans. We call 11 | it Trailmix. 12 |
13 | 14 |15 | It works just like OhLife used to: a daily email asking how 16 | your day went, plus an old entry chosen at random. Just as simple and 17 | beautiful as OhLife, but with a business model. 18 |
19 | 20 |21 | And, of course, you can import all your existing OhLife 22 | entries. 23 |
24 | 25 |26 | Your monthly payment ensures we'll never have ads and can justify the work to 27 | keep things running. No surprise shutdown with minimal notice. 28 |
29 | 30 |31 | <%= render "subscriptions/pricing" %> 32 |
33 | 34 |35 | You can sign up now and restart your journaling habit like 36 | nothing ever happened. Trailmix is your new private place to write. 37 |
38 | 39 | <%= render 'subscriptions/payment_form' %> 40 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Trailmix 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 8.0 13 | 14 | config.i18n.enforce_available_locales = true 15 | 16 | config.active_record.default_timezone = :utc 17 | 18 | config.generators do |generate| 19 | generate.helper false 20 | generate.javascript_engine false 21 | generate.request_specs false 22 | generate.routing_specs false 23 | generate.stylesheets false 24 | generate.test_framework :rspec 25 | generate.view_specs false 26 | end 27 | 28 | config.eager_load_paths += ["#{config.root}/app/workers"] 29 | 30 | ActionMailer::Base.default( 31 | from: "Team Trailmixthis is an email message
", 29 | SPF: "pass", 30 | attachments: 1, 31 | attachment1: photo_attachment 32 | } 33 | end 34 | 35 | def photo_attachment 36 | fixture_file_upload("photo.jpg", "image/jpg") 37 | end 38 | 39 | def app 40 | Rails.application 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/models/ohlife_importer.rb: -------------------------------------------------------------------------------- 1 | class OhlifeImporter 2 | def initialize(user, import) 3 | @user = user 4 | @import = import 5 | end 6 | 7 | def run 8 | export.each_line(separator) do |line| 9 | if is_start_of_entry?(line) 10 | build_new_entry(line) 11 | else 12 | append_to_existing_entry(line) 13 | end 14 | end 15 | 16 | save_entry 17 | end 18 | 19 | private 20 | 21 | attr_reader :user, :import 22 | 23 | def is_start_of_entry?(line) 24 | line =~ /\d\d\d\d-\d\d-\d\d/ 25 | end 26 | 27 | def build_new_entry(line) 28 | if @entry 29 | save_entry 30 | end 31 | 32 | @entry = Entry.new(body: "", user: user, import: import) 33 | @entry.date = line 34 | end 35 | 36 | def save_entry 37 | @entry.body.strip! 38 | @entry.save! 39 | end 40 | 41 | def append_to_existing_entry(line) 42 | line = convert_to_unix_line_endings(line) 43 | @entry.body.concat(line) 44 | end 45 | 46 | def convert_to_unix_line_endings(line) 47 | line.strip.concat("\n") 48 | end 49 | 50 | def export 51 | ohlife_export = import.ohlife_export 52 | 53 | if ohlife_export.file.is_a?(CarrierWave::SanitizedFile) 54 | ohlife_export.read 55 | else 56 | open(ohlife_export.url) 57 | end 58 | end 59 | 60 | def separator 61 | "\r\n" 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/transitions/fix_future_dated_entries_transition_spec.rb: -------------------------------------------------------------------------------- 1 | describe FixFutureDatedEntriesTransition do 2 | describe "#perform" do 3 | context "for entries dated in the past" do 4 | it "does nothing" do 5 | last_year = 1.year.ago.to_date 6 | yesterday = 1.day.ago.to_date 7 | entry_from_last_year = create(:entry, date: last_year) 8 | entry_from_yesterday = create(:entry, date: yesterday) 9 | 10 | FixFutureDatedEntriesTransition.perform 11 | 12 | entry_from_last_year.reload 13 | entry_from_yesterday.reload 14 | expect(entry_from_last_year.date).to eq(last_year) 15 | expect(entry_from_yesterday.date).to eq(yesterday) 16 | end 17 | end 18 | 19 | context "for entries dated in the future" do 20 | it "fixes the date by moving them one year back" do 21 | tomorrow = 1.day.from_now.to_date 22 | next_week = 1.week.from_now.to_date 23 | entry_from_tomorrow = create(:entry, date: tomorrow) 24 | entry_from_next_week = create(:entry, date: next_week) 25 | 26 | FixFutureDatedEntriesTransition.perform 27 | 28 | entry_from_tomorrow.reload 29 | entry_from_next_week.reload 30 | expect(entry_from_tomorrow.date).to eq(tomorrow - 1.year) 31 | expect(entry_from_next_week.date).to eq(next_week - 1.year) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/views/credit_cards/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :javascript do %> 2 | <%= render "stripe_javascript" %> 3 | <% end %> 4 | 5 | <% content_for :header do %> 6 |10 | <%= form_tag credit_card_path, method: :put, id: "credit-card-update" do %> 11 |
12 | 13 |51 | -------------------------------------------------------------------------------- /db/migrate/20140921195949_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | create_table :delayed_jobs, :force => true do |table| 4 | table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. 6 | table.text :handler, :null => false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base/_body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 16px; 3 | padding: 70px 0; 4 | } 5 | 6 | a { 7 | color: $brown; 8 | 9 | &:hover { 10 | color: $brown; 11 | } 12 | } 13 | 14 | p, h3, h2 { 15 | margin-bottom: 25px; 16 | } 17 | 18 | mark { 19 | color: $brown; 20 | background: $lighter-brown; 21 | } 22 | 23 | .btn-default, 24 | .pagination > li > a, 25 | .pagination > li > span { 26 | color: $brown; 27 | background: $lighter-brown; 28 | border-color: $light-brown; 29 | 30 | &:hover, &:focus, &:disabled { 31 | color: $brown; 32 | background: $light-brown; 33 | border-color: $light-brown; 34 | } 35 | } 36 | 37 | .pagination > .disabled > span, 38 | .pagination > .disabled > span:hover, 39 | .pagination > .disabled > span:focus, 40 | .pagination > .disabled > a, 41 | .pagination > .disabled > a:hover, 42 | .pagination > .disabled > a:focus { 43 | color: $transparent-brown; 44 | background: $light-brown; 45 | border-color: $light-brown; 46 | } 47 | 48 | .btn-primary { 49 | color: white; 50 | background: $brown; 51 | border-color: $brown; 52 | 53 | &:hover, &:focus, &:disabled { 54 | color: white; 55 | background: $brown; 56 | border-color: $brown; 57 | } 58 | } 59 | 60 | .alert-danger, .alert-success { 61 | color: $brown; 62 | background: $lighter-brown; 63 | border-color: $light-brown; 64 | } 65 | 66 | .jumbotron { 67 | background: $lighter-grey; 68 | padding: 0 0 20px 0; 69 | text-align: center; 70 | } 71 | -------------------------------------------------------------------------------- /db/migrate/20140921203919_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table(:users) do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.inet :current_sign_in_ip 20 | t.inet :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps 35 | end 36 | 37 | add_index :users, :email, unique: true 38 | add_index :users, :reset_password_token, unique: true 39 | # add_index :users, :confirmation_token, unique: true 40 | # add_index :users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/models/ohlife_importer_spec.rb: -------------------------------------------------------------------------------- 1 | describe OhlifeImporter do 2 | describe "#run" do 3 | it "creates entries for each entry in the file" do 4 | import = create_import 5 | user = import.user 6 | 7 | OhlifeImporter.new(user, import).run 8 | 9 | first_entry = user.entries.first 10 | expect(first_entry.body).to( 11 | eq("Older entry line one.\n\nOlder entry line two.") 12 | ) 13 | expect(first_entry.date).to eq(Date.new(2014, 1, 28)) 14 | 15 | second_entry = user.entries.second 16 | expect(second_entry.body).to( 17 | eq("List of items:\n\n* Item 1\n* Item 2") 18 | ) 19 | expect(second_entry.date).to eq(Date.new(2014, 1, 29)) 20 | end 21 | 22 | it "handles a single newline between lines" do 23 | import = create_import 24 | user = import.user 25 | 26 | OhlifeImporter.new(user, import).run 27 | 28 | first_entry = user.entries.first 29 | expect(first_entry.body).to( 30 | eq("Older entry line one.\n\nOlder entry line two.") 31 | ) 32 | end 33 | 34 | it "associates the created entries with an import" do 35 | import = create_import 36 | user = import.user 37 | 38 | OhlifeImporter.new(user, import).run 39 | 40 | expect(import.reload.entries.count).to eq(2) 41 | end 42 | end 43 | 44 | def create_import 45 | create(:import, ohlife_export: File.new(ohlife_export)) 46 | end 47 | 48 | def ohlife_export 49 | Rails.root + "spec/fixtures/files/ohlife_export.txt" 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/views/credit_cards/_stripe_javascript.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43 | -------------------------------------------------------------------------------- /spec/models/search_spec.rb: -------------------------------------------------------------------------------- 1 | describe Search do 2 | describe "#entries" do 3 | it "returns entries whose bodies match the search term" do 4 | user = create(:user) 5 | matched_entry = create(:entry, user: user, body: "I like cats") 6 | create(:entry, user: user, body: "I like dogs") 7 | create(:entry, body: "This guy likes cats too") 8 | search = Search.new(term: "cats", user: user) 9 | 10 | result = search.entries.map(&:body) 11 | 12 | expect(result).to eq([matched_entry.body]) 13 | end 14 | 15 | it "is not case sensitive" do 16 | user = create(:user) 17 | create(:entry, user: user, body: "i like pizza") 18 | create(:entry, user: user, body: "I REALLY LIKE PIZZA") 19 | all_user_entries = user.entries.map(&:body) 20 | search = Search.new(term: "piZZa", user: user) 21 | 22 | result = search.entries.map(&:body) 23 | 24 | expect(result).to contain_exactly(*all_user_entries) 25 | end 26 | 27 | it "sorts entries by date" do 28 | user = create(:user) 29 | create(:entry, user: user, body: "I like cats.", date: 2.days.ago) 30 | create(:entry, user: user, body: "I still like cats.", date: 1.day.ago) 31 | entries_sorted_by_date = user.entries.by_date.map(&:body) 32 | search = Search.new(term: "cats", user: user) 33 | 34 | result = search.entries.map(&:body) 35 | 36 | expect(result).to eq(entries_sorted_by_date) 37 | end 38 | 39 | it "returns nothing when there's no search term" do 40 | entry = create(:entry, body: "I like cats") 41 | search = Search.new(user: entry.user) 42 | 43 | result = search.entries 44 | 45 | expect(result).to be_empty 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby file: ".ruby-version" 4 | 5 | gem "airbrake" 6 | gem "autoprefixer-rails" 7 | gem "benchmark" 8 | gem "bigdecimal", "~> 3.1.9" 9 | gem "binding_of_caller" 10 | gem "carrierwave" 11 | gem "chartkick" 12 | gem "devise", "~> 4.9.4" 13 | gem "drb" 14 | gem "email_reply_parser" 15 | gem "email_validator" 16 | gem "flutie" 17 | gem "fog" 18 | gem "griddler" 19 | gem "griddler-sendgrid" 20 | gem "groupdate" 21 | gem "high_voltage" 22 | gem "jquery-rails" 23 | gem "kaminari" 24 | gem "mini_magick" 25 | gem "mutex_m" 26 | gem "normalize-rails", "~> 3.0.0" 27 | gem "ostruct" 28 | gem "pg", "~> 1.0" 29 | gem "puma" 30 | gem "rack-timeout" 31 | gem "rails", "~> 8.0.0" 32 | gem "recipient_interceptor" 33 | gem "redis" 34 | gem "sass-rails", "~> 6.0.0" 35 | gem "sidekiq", "< 8" 36 | gem "sprockets", ">= 2.12.5" 37 | gem "simple_form", "~> 5.0.3" 38 | gem "sinatra", "~> 4.1.0", require: false 39 | gem "stripe", "~> 1.57.1" 40 | gem "title" 41 | gem "xmlrpc" 42 | 43 | group :development do 44 | gem "better_errors" 45 | gem "spring" 46 | gem "spring-commands-rspec" 47 | gem "web-console", "~> 2.0" 48 | end 49 | 50 | group :development, :test do 51 | gem "awesome_print" 52 | gem "dotenv-rails" 53 | gem "factory_bot_rails" 54 | gem "pry-rails" 55 | gem "rspec-activemodel-mocks" 56 | gem "rspec-rails", "~> 6.1.0" 57 | end 58 | 59 | group :test do 60 | gem "selenium-webdriver" 61 | gem "database_cleaner" 62 | gem "formulaic" 63 | gem "stripe-ruby-mock", "~> 2.4.0" 64 | gem "launchy" 65 | gem "timecop" 66 | gem "webmock" 67 | gem "rails-controller-testing" 68 | end 69 | 70 | group :staging, :production do 71 | gem "rails_12factor", "~> 0.0.3" 72 | gem "rails_serve_static_assets", "~> 0.0.4" 73 | end 74 | -------------------------------------------------------------------------------- /app/views/application/_navbar.html.erb: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /spec/models/entry_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Entry, :type => :model do 2 | describe ".by_date" do 3 | it "sorts entries, by date, with the newest first" do 4 | user = create(:user) 5 | 6 | entries = [ 7 | create(:entry, user: user, date: Date.current, created_at: 1.day.ago), 8 | create(:entry, user: user, date: 1.days.ago, created_at: 3.days.ago), 9 | create(:entry, user: user, date: 2.days.ago, created_at: Time.current), 10 | create(:entry, user: user, date: 3.day.ago, created_at: 2.days.ago), 11 | ] 12 | 13 | entries_by_date = user.entries.by_date 14 | 15 | expect(entries_by_date).to( 16 | eq(entries), 17 | "expected #{entries.map(&:id)}, got #{entries_by_date.map(&:id)}") 18 | end 19 | end 20 | 21 | describe ".newest" do 22 | it "returns the newest dated entry" do 23 | user = create(:user) 24 | newest_entry = create(:entry, user: user, date: Date.current) 25 | create(:entry, user: user, date: 1.day.ago) 26 | 27 | entry = user.entries.newest 28 | 29 | expect(entry).to eq(newest_entry) 30 | end 31 | end 32 | 33 | describe ".random" do 34 | it "returns a random entry" do 35 | entry = create(:entry) 36 | 37 | expect(Entry.random).to eq(entry) 38 | end 39 | end 40 | 41 | describe "#for_today?" do 42 | context "when the entry is dated for today" do 43 | it "returns true" do 44 | Timecop.freeze(Time.utc(2014, 1, 1, 20)) do 45 | user = create(:user, time_zone: "UTC") 46 | entry = create(:entry, user: user, date: Time.zone.now) 47 | 48 | expect(entry).to be_for_today 49 | 50 | user.update_attribute(:time_zone, "Guam") # UTC+10 51 | 52 | expect(entry).to_not be_for_today 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/transitions/backfill_subscriptions_transition_spec.rb: -------------------------------------------------------------------------------- 1 | describe BackfillSubscriptionsTransition do 2 | describe "#perform" do 3 | context "for users missing a Subscription" do 4 | it "creates one with the correct Stripe customer id" do 5 | first_user = create(:user, subscription: nil) 6 | second_user = create(:user, subscription: nil) 7 | stub_stripe_customers([ 8 | { email: first_user.email, id: "first id" }, 9 | { email: second_user.email, id: "second id" } 10 | ]) 11 | 12 | BackfillSubscriptionsTransition.perform 13 | 14 | first_subscription = first_user.reload.subscription 15 | second_subscription = second_user.reload.subscription 16 | 17 | expect(first_subscription.stripe_customer_id).to eq("first id") 18 | expect(second_subscription.stripe_customer_id).to eq("second id") 19 | end 20 | end 21 | 22 | context "for users that already have a Subscription" do 23 | it "does nothing" do 24 | original_subscription = create(:subscription) 25 | user = create(:user, subscription: original_subscription) 26 | stub_stripe_customers([ 27 | { email: user.email, id: "new id" } 28 | ]) 29 | 30 | BackfillSubscriptionsTransition.perform 31 | 32 | expect(user.reload.subscription).to eq(original_subscription) 33 | end 34 | end 35 | end 36 | 37 | def stub_stripe_customers(customers) 38 | customer_list = double("Stripe::ListObject") 39 | iterator = allow(customer_list).to(receive(:each)) 40 | 41 | customers.inject(iterator) do |iterator, customer| 42 | iterator.and_yield( 43 | double("Stripe::Customer", email: customer[:email], id: customer[:id])) 44 | end 45 | 46 | allow(Stripe::Customer).to(receive(:all).and_return(customer_list)) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/views/subscriptions/_close_account.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= link_to "Close my account", 3 | "#", 4 | data: { toggle: "modal", target: "#closeAccount" } 5 | %> 6 | 7 | 8 |
66 | -------------------------------------------------------------------------------- /app/views/pages/faq.html.erb: -------------------------------------------------------------------------------- 1 |2 |
8 | Trailmix costs $3.99 per month. 9 |
10 | 11 |14 | Yes! Your first 14 days of Trailmix are free. You'll be asked for a credit card 15 | when you sign up, but you will not be charged until your trial is 16 | over. 17 |
18 | 19 |22 | No problem! We'll happily issue you a refund. 23 |
24 | 25 |28 | That's fine. We will refund your latest Trailmix payment for any reason at all. 29 | Just try us! 30 |
31 | 32 |35 | It costs money to run servers, send emails (really!), and to pay ourselves a 36 | little bit to work on the service. We want Trailmix to be around for a long 37 | time, so it's important that it be self-sustaining. 38 |
39 | 40 |43 | Yep! It takes two clicks and ten seconds. Visit the 44 | <%= link_to "import page", new_import_path %> to get things started. 45 |
46 | 47 |50 | Yeah! Visit the <%= link_to "export page", new_export_path %> to download 51 | them all. 52 |
53 | 54 |57 | Yep! Just visit the <%= link_to "settings page", edit_settings_path %>. 58 |
59 | 60 |63 | Yes indeed! Just attach an image to your email and we'll make it part of your 64 | entry. 65 |
66 | 67 |70 | Absolutely. Read our <%= link_to "privacy policy", page_path(:privacy) %>. 71 |
72 | 73 |76 | Trailmix was built by Ben Orenstein and 77 | Chris Hunt in the Fall of 2014. 78 |
79 | 80 |81 | <%= image_tag "team.jpg", class: "img-responsive img-rounded" %> 82 |
83 | -------------------------------------------------------------------------------- /spec/models/prompt_delivery_hour_spec.rb: -------------------------------------------------------------------------------- 1 | describe PromptDeliveryHour do 2 | after { Timecop.return } 3 | 4 | context "when not in daylight savings" do 5 | before { Timecop.freeze Time.utc(2014, 10, 1) } 6 | 7 | describe "#in_time_zone" do 8 | it "returns the hour in the specified time zone" do 9 | hour = 15 10 | time_zone = "Eastern Time (US & Canada)" # UTC-4 11 | 12 | delivery_hour = PromptDeliveryHour.new(hour, time_zone).in_time_zone 13 | 14 | expect(delivery_hour).to eq(11) 15 | end 16 | 17 | it "always returns an hour in the 24 hour range" do 18 | hour = 1 19 | time_zone = "Eastern Time (US & Canada)" # UTC-4 20 | 21 | delivery_hour = PromptDeliveryHour.new(hour, time_zone).in_time_zone 22 | 23 | expect(delivery_hour).to eq(21) 24 | end 25 | end 26 | 27 | describe "#in_utc" do 28 | it "returns the hour in the utc time zone" do 29 | hour = 15 30 | time_zone = "Eastern Time (US & Canada)" # UTC-4 31 | 32 | delivery_hour = PromptDeliveryHour.new(hour, time_zone).in_utc 33 | 34 | expect(delivery_hour).to eq(19) 35 | end 36 | 37 | it "always returns an hour in the 24 hour range" do 38 | hour = 20 39 | time_zone = "Eastern Time (US & Canada)" # UTC-4 40 | 41 | delivery_hour = PromptDeliveryHour.new(hour, time_zone).in_utc 42 | 43 | expect(delivery_hour).to eq(0) 44 | end 45 | end 46 | end 47 | 48 | context "when in daylight savings" do 49 | before { Timecop.freeze Time.utc(2014, 1, 1) } 50 | 51 | describe "#in_time_zone" do 52 | it "returns the hour in the specified time zone accounting for DST" do 53 | hour = 15 54 | time_zone = "Eastern Time (US & Canada)" # UTC-5 55 | 56 | delivery_hour = PromptDeliveryHour.new(hour, time_zone).in_time_zone 57 | 58 | expect(delivery_hour).to eq(10) 59 | end 60 | end 61 | 62 | describe "#in_utc" do 63 | it "returns the hour in the utc time zone accounting for DST" do 64 | hour = 15 65 | time_zone = "Eastern Time (US & Canada)" # UTC-5 66 | 67 | delivery_hour = PromptDeliveryHour.new(hour, time_zone).in_utc 68 | 69 | expect(delivery_hour).to eq(20) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/controllers/cancellations_controller_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe CancellationsController do 3 | let(:stripe_helper) { StripeMock.create_test_helper } 4 | before { StripeMock.start } 5 | after { StripeMock.stop } 6 | 7 | describe "#create" do 8 | context "when signed in" do 9 | it "destroys the current user" do 10 | stripe_customer = Stripe::Customer.create 11 | subscription = create(:subscription, stripe_customer_id: stripe_customer.id) 12 | user = subscription.user 13 | 14 | sign_in(user) 15 | post :create, params: { id: user.id } 16 | 17 | expect(User.count).to be_zero 18 | end 19 | 20 | it "creates a Cancellation" do 21 | stripe_customer = Stripe::Customer.create 22 | subscription = create(:subscription, stripe_customer_id: stripe_customer.id) 23 | user = subscription.user 24 | 25 | sign_in(user) 26 | post :create, params: { id: user.id, reason: "I'm done journaling" } 27 | cancellation = Cancellation.last 28 | 29 | expect(cancellation.reason).to eq("I'm done journaling") 30 | expect(cancellation.stripe_customer_id).to( 31 | eq(subscription.stripe_customer_id) 32 | ) 33 | end 34 | 35 | it "cancels the Stripe subscription" do 36 | subscription = create(:subscription) 37 | user = subscription.user 38 | stripe_subscription = 39 | stub_stripe(subscription.stripe_customer_id) 40 | sign_in(user) 41 | 42 | post :create, params: { id: user.id } 43 | 44 | expect(stripe_subscription).to(have_received(:delete)) 45 | end 46 | end 47 | 48 | context "when signed out" do 49 | it "redirects to sign in" do 50 | post :create, params: { id: "not an id" } 51 | 52 | expect(response).to redirect_to(new_user_session_path) 53 | end 54 | end 55 | 56 | def stub_stripe(customer_id) 57 | double("Stripe::Subscription", delete: true).tap do |subscription| 58 | customer = double( 59 | "Stripe::Customer", 60 | id: customer_id, 61 | subscriptions: [subscription] 62 | ) 63 | 64 | allow(Stripe::Customer).to( 65 | receive(:retrieve).with(customer_id).and_return(customer) 66 | ) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV["CI"].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } 20 | 21 | # Show full error reports. 22 | config.consider_all_requests_local = true 23 | config.cache_store = :null_store 24 | 25 | # Render exception templates for rescuable exceptions and raise for other exceptions. 26 | config.action_dispatch.show_exceptions = :rescuable 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory. 32 | config.active_storage.service = :test 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Set host to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = { host: "trailmix.life" } 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | 51 | # Raise error when a before_action's only/except options reference missing actions. 52 | config.action_controller.raise_on_missing_callback_actions = true 53 | end 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Trailmix 2 | ======== 3 | 4 | What is this? 5 | ------------- 6 | This is source code behind [Trailmix], a private place to write. 7 | 8 | Every day, Trailmix sends you an email asking how your day went. Simply reply 9 | to that email, and a journal entry is created. 10 | 11 | To encourage you to write, and to provide a delightful reminiscence, each 12 | prompt email contains a previous entry chosen at random. 13 | 14 |  15 | 16 | [Trailmix]: https://www.trailmix.life/pages/github 17 | 18 | Caveat emptor 19 | ------------- 20 | 21 | This source code is very much provided as-is. 22 | 23 | We hope you find it useful to see the source code behind a production 24 | application that real people are paying for. 25 | 26 | You are welcome to do what you like with this code, including running your own 27 | instance of it. However, please know that our development time is limited, so 28 | we're unable to spend time helping you make it work. 29 | 30 | Who wrote this? 31 | --------------- 32 | 33 | [Chris] and [Ben] did. 34 | 35 | [Chris]: http://twitter.com/chrishunt 36 | [Ben]: http://twitter.com/r00k 37 | 38 | Contributions 39 | ------------- 40 | 41 | Our goal is to keep the functionality of this app very focused. Please ask 42 | before opening a pull to add features. 43 | 44 | That said, if you find a bug, please do open an issue! 45 | 46 | Getting Started 47 | --------------- 48 | 49 | After you have cloned this repo, run this setup script to set up your machine 50 | with the necessary dependencies to run and test this app: 51 | 52 | % ./bin/setup 53 | 54 | The script also assumes you have a machine equipped with Ruby, Postgres, etc. 55 | If not, set up your machine with [this script]. 56 | 57 | [this script]: https://github.com/thoughtbot/laptop 58 | 59 | After setting up, you can run the application using [foreman]: 60 | 61 | % foreman start 62 | 63 | If you don't have `foreman`, see [Foreman's install instructions][foreman]. It 64 | is [purposefully excluded from the project's `Gemfile`][exclude]. 65 | 66 | [foreman]: https://github.com/ddollar/foreman 67 | [exclude]: https://github.com/ddollar/foreman/pull/437#issuecomment-41110407 68 | 69 | Guidelines 70 | ---------- 71 | 72 | Use the following guides for getting things done, programming well, and 73 | programming in style. 74 | 75 | * [Protocol](http://github.com/thoughtbot/guides/blob/master/protocol) 76 | * [Best Practices](http://github.com/thoughtbot/guides/blob/master/best-practices) 77 | * [Style](http://github.com/thoughtbot/guides/blob/master/style) 78 | -------------------------------------------------------------------------------- /app/views/landing/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :header do %> 2 | <%= render 'header' %> 3 | <% end %> 4 | 5 |6 | Hey there, 7 |
8 | 9 |10 | Want to know the hardest part about keeping a journal? 11 | Actually keeping one. 12 |
13 | 14 |15 | Like most habits, it's easy for the first few days, but soon a lack of 16 | motivation or simple forgetfulness sets in. 17 |
18 | 19 |That's why we built Trailmix.
20 | 21 |22 | Every day, Trailmix sends you an email asking how your day 23 | went. All you do is hit reply and start typing. You can also 24 | include a photo that will remind you of your day. When you're done, hit send, 25 | and your entry will be saved instantly. 26 |
27 | 28 |29 | With every reminder email, we'll include one of your previous entries 30 | chosen at random. You might get one from yesterday, last week, or 31 | last year. 32 |
33 | 34 |35 | This makes opening our emails fun! It's delightful to be reminded of your 36 | former thoughts and feelings. Even yesterday's entry can surprise you. 37 |
38 | 39 |40 | This enjoyable feeling will encourage you to write more, so that future-you 41 | can enjoy reading today's entry. 42 |
43 | 44 |45 | Before you know it, you're a daily journaler. 46 |
47 | 48 |49 | Once you've built this habit, you get a tremendous payoff: the ability to 50 | look back over your life experiences. You'll notice patterns like "I always 51 | seem stressed on Wednesdays!" Or "Wow, I was so worried about that talk with 52 | my boss, but it turned out completely fine." 53 |
54 | 55 |56 | Insights like these can have a profound effect on your happiness and 57 | behavior. We know, because we've experienced it ourselves. 58 |
59 | 60 |If you'd like to experience this too, you can sign up below.
61 | 62 |63 | <%= render "subscriptions/pricing" %> 64 |
65 | 66 |67 | We charge for Trailmix because it costs money to run servers and fix bugs, 68 | but we're not in this to get rich. We're in it because we believe 69 | daily journalers are happier people and we want you to become one. 70 |
71 | 72 |73 | — <%= link_to "Ben and Chris", page_path(:faq) + "#us" %> 74 |
75 | 76 |77 | 78 | P.S. If you're a former OhLife user, you can import all your old entries 79 | with two clicks. 80 | 81 |
82 | 83 | <%= render "subscriptions/payment_form" %> 84 | <%= render "subscriptions/sign_up_link_javascript" %> 85 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } 24 | else 25 | config.action_controller.perform_caching = false 26 | end 27 | 28 | # Change to :null_store to avoid any caching. 29 | config.cache_store = :memory_store 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | # Make template changes take effect immediately. 38 | config.action_mailer.perform_caching = false 39 | 40 | # Set localhost to be used by links generated in mailer templates. 41 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 } 42 | 43 | # Print deprecation notices to the Rails logger. 44 | config.active_support.deprecation = :log 45 | 46 | # Raise an error on page load if there are pending migrations. 47 | config.active_record.migration_error = :page_load 48 | 49 | # Highlight code that triggered database queries in logs. 50 | config.active_record.verbose_query_logs = true 51 | 52 | # Append comments with runtime information tags to SQL queries in logs. 53 | config.active_record.query_log_tags_enabled = true 54 | 55 | # Highlight code that enqueued background job in logs. 56 | config.active_job.verbose_enqueue_logs = true 57 | 58 | # Raises error for missing translations. 59 | config.i18n.raise_on_missing_translations = true 60 | 61 | # Annotate rendered view with file names. 62 | config.action_view.annotate_rendered_view_with_filenames = true 63 | 64 | # Uncomment if you wish to allow Action Cable access from any origin. 65 | # config.action_cable.disable_request_forgery_protection = true 66 | 67 | # Raise error when a before_action's only/except options reference missing actions. 68 | config.action_controller.raise_on_missing_callback_actions = true 69 | end 70 | -------------------------------------------------------------------------------- /spec/controllers/subscriptions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | describe SubscriptionsController, sidekiq: :inline do 2 | describe "#create" do 3 | before do 4 | stub_sign_in 5 | stub_stripe_customer_create 6 | end 7 | 8 | context "with valid params" do 9 | it "creates a user, subscription, stripe customer, and email" do 10 | post :create, params: default_params 11 | 12 | expect(User.count).to eq(1) 13 | expect(Subscription.count).to eq(1) 14 | expect(Stripe::Customer).to( 15 | have_received(:create).with( 16 | email: default_params[:email], 17 | card: default_params[:stripe_card_id], 18 | plan: Rails.configuration.stripe[:plan_name])) 19 | expect(ActionMailer::Base.deliveries).not_to be_empty 20 | end 21 | 22 | it "saves the Stripe customer id on the subscription" do 23 | stripe_customer_id = "cus_123" 24 | stub_stripe_customer_create(stripe_customer_id) 25 | 26 | post :create, params: default_params 27 | subscription = Subscription.last 28 | 29 | expect(subscription.stripe_customer_id).to eq(stripe_customer_id) 30 | end 31 | end 32 | 33 | context "when the user is invalid" do 34 | it "does not create a new user, subscription, or stripe customer" do 35 | stub_invalid_user 36 | 37 | post :create, params: default_params 38 | 39 | expect(User.count).to eq(0) 40 | expect(Subscription.count).to eq(0) 41 | expect(ActionMailer::Base.deliveries).to be_empty 42 | end 43 | end 44 | 45 | context "when the credit card fails to charge" do 46 | it "does not create a user, subscription, or stripe customer" do 47 | stub_stripe_charge_failure 48 | 49 | post :create, params: default_params 50 | 51 | expect(User.count).to eq(0) 52 | expect(Subscription.count).to eq(0) 53 | expect(ActionMailer::Base.deliveries).to be_empty 54 | end 55 | end 56 | 57 | def default_params 58 | { 59 | email: 'foo@bar.com', 60 | password: 'password', 61 | stripe_card_id: 'abc123' 62 | } 63 | end 64 | 65 | def stub_stripe_charge_failure 66 | error = Stripe::CardError.new(double, double, double) 67 | allow(error).to(receive(:message).and_return("Failed to charge card")) 68 | 69 | allow(Stripe::Customer).to(receive(:create).and_raise(error)) 70 | end 71 | 72 | def stub_stripe_customer_create(stripe_customer_id = "cus_123") 73 | allow(Stripe::Customer).to( 74 | receive(:create).and_return( 75 | double("Stripe::Customer", id: stripe_customer_id))) 76 | end 77 | 78 | def stub_sign_in 79 | allow(controller).to receive(:sign_in) 80 | end 81 | 82 | def stub_invalid_user 83 | user = double("user", 84 | valid?: false, 85 | errors: double('errors').as_null_object) 86 | allow(User).to(receive(:new).and_return(user)) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe User, :type => :model do 2 | describe ".promptable" do 3 | it "returns promptable users for the current hour" do 4 | Timecop.freeze(Time.utc(2014, 1, 1, 11)) do # 11AM UTC 5 | create(:user, time_zone: "UTC", prompt_delivery_hour: 10) 6 | create(:user, time_zone: "UTC", prompt_delivery_hour: 12) 7 | utc_11am = create(:user, time_zone: "UTC", prompt_delivery_hour: 11) 8 | 9 | expect(User.promptable).to eq [utc_11am] 10 | end 11 | end 12 | end 13 | 14 | describe "#reply_token" do 15 | it "is generated automatically before saving" do 16 | user = build(:user, reply_token: nil) 17 | 18 | user.save 19 | 20 | expect(user.reply_token).to_not be_nil 21 | end 22 | end 23 | 24 | describe "#reply_email" do 25 | it "starts with the reply token" do 26 | user = create(:user) 27 | 28 | expect(user.reply_email).to start_with(user.reply_token) 29 | end 30 | 31 | it "ends with the email domain" do 32 | user = create(:user) 33 | email_domain = ENV.fetch("SMTP_DOMAIN") 34 | 35 | expect(user.reply_email).to end_with("@#{email_domain}") 36 | end 37 | end 38 | 39 | describe "#newest_entry" do 40 | it "returns the newest entry by date" do 41 | user = create(:user) 42 | newest_entry = create(:entry, user: user, date: 1.day.ago) 43 | oldest_entry = create(:entry, user: user, date: 2.days.ago) 44 | 45 | expect(user.newest_entry).to eq(newest_entry) 46 | end 47 | end 48 | 49 | describe "#prompt_entry" do 50 | it "delegates to PromptEntry" do 51 | user = create(:user) 52 | allow(PromptEntry).to( 53 | receive(:best).with(user.entries). 54 | and_return("best entry")) 55 | 56 | entry = user.prompt_entry 57 | 58 | expect(entry).to eq("best entry") 59 | end 60 | end 61 | 62 | describe "#prompt_delivery_hour" do 63 | it "returns the prompt delivery hour in the user's time zone" do 64 | Timecop.freeze(Time.utc(2014, 10, 1)) do 65 | user = create(:user, time_zone: "Melbourne") # Melbourne is UTC+10 66 | user.update_column :prompt_delivery_hour, 5 67 | 68 | prompt_delivery_hour = user.prompt_delivery_hour 69 | 70 | expect(prompt_delivery_hour).to eq(15) 71 | end 72 | end 73 | end 74 | 75 | describe "#prompt_delivery_hour=" do 76 | it "writes the prompt delivery hour in utc" do 77 | Timecop.freeze(Time.utc(2014, 10, 1)) do 78 | user = create(:user, time_zone: "Melbourne") # Melbourne is UTC+10 79 | 80 | user.prompt_delivery_hour = 5 81 | prompt_delivery_hour = user.read_attribute(:prompt_delivery_hour) 82 | 83 | expect(prompt_delivery_hour).to eq(19) 84 | end 85 | end 86 | end 87 | 88 | describe "#stripe_customer_id" do 89 | it "delegates to the subscription" do 90 | subscription = create(:subscription) 91 | user = subscription.user 92 | 93 | expect(user.stripe_customer_id).to eq(subscription.stripe_customer_id) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /app/views/pages/keeping-a-development-journal.html.erb: -------------------------------------------------------------------------------- 1 |2 |
6 | tl;dr: Taking 60 seconds to jot some notes about your day can yield impressive 7 | professional benefits. <%= link_to 'Trailmix', new_registration_path %> makes 8 | it particularly easy to build this habit. 9 |
10 | 11 |16 | Have you ever gotten an error and said "I know I've seen this 17 | before, but how did I solve it?". With a searchable journal, that annoying 18 | experience goes away. 19 |
20 | 21 |27 | The true test of a design isn't when you first write it, it's when you need 28 | to change it later. 29 |
30 | 31 |32 | If you make a note when your design choices work out particularly well or 33 | poorly, you'll soon build a catalog of techniques to embrace or avoid. After 34 | your third "Observer pattern on app_foo continues to confuse new developers 35 | coming on to the project", you'll learn to shy away in the future. 36 |
37 | 38 |43 | Emailing your boss a week before your review reminding him or her of your 44 | major accomplishments in the past N months is a very smart move. 45 |
46 | 47 |52 | As humans, we tend to have some good days and some bad ones. Because of this, 53 | it can be hard to detect trends in your mood. Consistent writing can help you 54 | detect that the bad days are consistently outnumbering the good ones. Maybe 55 | it's time to start looking. 56 |
57 | 58 |61 | Starting a development journal isn't hard. Maintaining one 62 | is. 63 |
64 | 65 |66 | The key to building a habit isn't initial excitement, it's consistency. The 67 | biggest enemy of consistency is forgetfulness. 68 |
69 | 70 |71 | If you'd like to try building this habit, you'll want to set a daily reminder 72 | for at least the first few weeks. When the alarm goes off, fire up your 73 | editor and record an entry. 74 |
75 | 76 |77 | Another option is an app we wrote called 78 | <%= link_to "Trailmix", new_registration_path %>. 79 |
80 | 81 |82 | Trailmix keeps you consistent by sending you an email each day asking what 83 | you were working on. You reply right in your email client and your entries 84 | are parsed into a searchable daily journal. 85 |
86 | 87 |88 | What's more, Trailmix includes an old entry chosen at random in your daily 89 | reminder. This keeps your previous entries fresh in your mind. You might 90 | never forget how you fixed an error again. 91 |
92 | 93 |Line 1
\r\n\r\nLine 2
") 75 | ) 76 | end 77 | 78 | context "and it has a photo" do 79 | it "includes it" do 80 | user = create(:user) 81 | entry = create(:entry, :with_photo, user: user) 82 | 83 | mail = PromptMailer.prompt(user, entry) 84 | 85 | expect(mail.body.encoded).to( 86 | include(entry.photo.url) 87 | ) 88 | end 89 | end 90 | end 91 | end 92 | 93 | context "when the user has no previous entries" do 94 | it "does not try to display the entry" do 95 | user = create(:user, entries: []) 96 | 97 | mail = PromptMailer.prompt(user, nil) 98 | 99 | expect(mail.body.encoded).to_not include("Remember this?") 100 | end 101 | end 102 | 103 | context "when there is an announcement" do 104 | it "shows the announcement in the prompt" do 105 | user = create(:user) 106 | announcement = "check out this new feature
" 107 | 108 | with_announcement(announcement) do 109 | mail = PromptMailer.prompt(user, nil) 110 | 111 | expect(mail.body.encoded).to include(announcement) 112 | end 113 | end 114 | end 115 | 116 | context "when a date is provided" do 117 | it "sends a prompt for the provided date" do 118 | user = create(:user) 119 | date = Date.parse("2017-10-31") 120 | 121 | mail = PromptMailer.prompt(user, nil, date) 122 | 123 | expect(mail.subject).to eq("It's Tuesday, Oct 31. How was your day?") 124 | end 125 | end 126 | 127 | def with_announcement(announcement) 128 | ENV["ANNOUNCEMENT"] = announcement 129 | yield 130 | ENV["ANNOUNCEMENT"] = nil 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.
109 |