minimum_viable_product 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +37 -0
- data/app/assets/javascripts/minimum_viable_product/application.js.erb +38 -0
- data/app/assets/javascripts/minimum_viable_product/init/controllers.js +23 -0
- data/app/assets/stylesheets/minimum_viable_product/application.scss +23 -0
- data/app/assets/stylesheets/minimum_viable_product/components/buttons.scss +20 -0
- data/app/assets/stylesheets/minimum_viable_product/components/fonts.scss +15 -0
- data/app/assets/stylesheets/minimum_viable_product/components/spacing.scss +13 -0
- data/app/assets/stylesheets/minimum_viable_product/components/typography.scss +40 -0
- data/app/assets/stylesheets/minimum_viable_product/init/_reset.scss +43 -0
- data/app/assets/stylesheets/minimum_viable_product/init/layout.scss +42 -0
- data/app/assets/stylesheets/minimum_viable_product/layout/basic.scss +6 -0
- data/app/assets/stylesheets/minimum_viable_product/layout/carousel.scss +128 -0
- data/app/assets/stylesheets/minimum_viable_product/layout/cover.scss +111 -0
- data/app/assets/stylesheets/minimum_viable_product/pages/sample.scss +14 -0
- data/app/assets/stylesheets/minimum_viable_product/skin/style.scss +8 -0
- data/app/controllers/minimum_viable_product/analytics_controller.rb +14 -0
- data/app/controllers/minimum_viable_product/concerns/analytics_concern.rb +77 -0
- data/app/controllers/minimum_viable_product/concerns/seo_concern.rb +25 -0
- data/app/controllers/minimum_viable_product/concerns/session_concern.rb +40 -0
- data/app/controllers/minimum_viable_product/controller.rb +11 -0
- data/app/controllers/minimum_viable_product/seo_controller.rb +13 -0
- data/app/controllers/minimum_viable_product/styleguide_controller.rb +17 -0
- data/app/helpers/minimum_viable_product/analytics_helper.rb +4 -0
- data/app/helpers/minimum_viable_product/application_helper.rb +15 -0
- data/app/helpers/minimum_viable_product/bootstrap_helper.rb +38 -0
- data/app/helpers/minimum_viable_product/seo_helper.rb +4 -0
- data/app/helpers/minimum_viable_product/styleguide_helper.rb +4 -0
- data/app/models/concerns/slugification.rb +16 -0
- data/app/views/layouts/minimum_viable_product/_header.html.erb +48 -0
- data/app/views/layouts/minimum_viable_product/_instrumentation.html.erb +75 -0
- data/app/views/layouts/minimum_viable_product/_meta.html.erb +37 -0
- data/app/views/layouts/minimum_viable_product/application.html.erb +24 -0
- data/app/views/layouts/minimum_viable_product/basic.html.erb +10 -0
- data/app/views/layouts/minimum_viable_product/carousel.html.erb +13 -0
- data/app/views/layouts/minimum_viable_product/cover.html.erb +13 -0
- data/bin/mvp +68 -0
- data/config/initializers/canonical_host.rb +3 -0
- data/config/initializers/client_side_validations.rb +20 -0
- data/config/initializers/cloudinary.rb +6 -0
- data/config/initializers/geocoder.rb +9 -0
- data/config/initializers/iteration.rb +33 -0
- data/config/initializers/rollbar.rb +59 -0
- data/config/initializers/segment.rb +5 -0
- data/config/initializers/simple_form.rb +165 -0
- data/config/initializers/simple_form_bootstrap.rb +149 -0
- data/config/initializers/spoof_ip.rb +33 -0
- data/config/initializers/ssl.rb +3 -0
- data/config/routes.rb +16 -0
- data/lib/minimum_viable_product/engine.rb +20 -0
- data/lib/minimum_viable_product/ext/nil.rb +5 -0
- data/lib/minimum_viable_product/ext/string.rb +7 -0
- data/lib/minimum_viable_product/version.rb +3 -0
- data/lib/minimum_viable_product.rb +6 -0
- data/lib/tasks/sitemap.rake +42 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/navigation_test.rb +8 -0
- data/test/minimum_viable_product_test.rb +7 -0
- data/test/test_helper.rb +21 -0
- metadata +595 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
body.body-cover {
|
2
|
+
text-align: center;
|
3
|
+
|
4
|
+
.site-wrapper {
|
5
|
+
display: table;
|
6
|
+
width: 100%;
|
7
|
+
height: 100%; /* For at least Firefox */
|
8
|
+
min-height: 100%;
|
9
|
+
}
|
10
|
+
.site-wrapper-inner {
|
11
|
+
display: table-cell;
|
12
|
+
vertical-align: top;
|
13
|
+
}
|
14
|
+
.cover-container {
|
15
|
+
margin-right: auto;
|
16
|
+
margin-left: auto;
|
17
|
+
}
|
18
|
+
|
19
|
+
/* Padding for spacing */
|
20
|
+
.inner {
|
21
|
+
padding: 30px;
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
/*
|
26
|
+
* Header
|
27
|
+
*/
|
28
|
+
.masthead-brand {
|
29
|
+
margin-top: 10px;
|
30
|
+
margin-bottom: 10px;
|
31
|
+
}
|
32
|
+
|
33
|
+
.masthead-nav > li {
|
34
|
+
display: inline-block;
|
35
|
+
}
|
36
|
+
.masthead-nav > li + li {
|
37
|
+
margin-left: 20px;
|
38
|
+
}
|
39
|
+
.masthead-nav > li > a {
|
40
|
+
padding-right: 0;
|
41
|
+
padding-left: 0;
|
42
|
+
font-size: 16px;
|
43
|
+
font-weight: bold;
|
44
|
+
}
|
45
|
+
.masthead-nav > li > a:hover,
|
46
|
+
.masthead-nav > li > a:focus {
|
47
|
+
background-color: transparent;
|
48
|
+
}
|
49
|
+
.masthead-nav > .active > a,
|
50
|
+
.masthead-nav > .active > a:hover,
|
51
|
+
.masthead-nav > .active > a:focus {
|
52
|
+
|
53
|
+
}
|
54
|
+
|
55
|
+
@media (min-width: 768px) {
|
56
|
+
.masthead-brand {
|
57
|
+
float: left;
|
58
|
+
}
|
59
|
+
.masthead-nav {
|
60
|
+
float: right;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
|
65
|
+
/*
|
66
|
+
* Cover
|
67
|
+
*/
|
68
|
+
|
69
|
+
.cover {
|
70
|
+
padding: 0 20px;
|
71
|
+
}
|
72
|
+
.cover .btn-lg {
|
73
|
+
padding: 10px 20px;
|
74
|
+
font-weight: bold;
|
75
|
+
}
|
76
|
+
|
77
|
+
|
78
|
+
/*
|
79
|
+
* Affix and center
|
80
|
+
*/
|
81
|
+
|
82
|
+
@media (min-width: 768px) {
|
83
|
+
/* Pull out the header and footer */
|
84
|
+
.masthead {
|
85
|
+
position: fixed;
|
86
|
+
top: 0;
|
87
|
+
}
|
88
|
+
.mastfoot {
|
89
|
+
position: fixed;
|
90
|
+
bottom: 0;
|
91
|
+
}
|
92
|
+
/* Start the vertical centering */
|
93
|
+
.site-wrapper-inner {
|
94
|
+
vertical-align: middle;
|
95
|
+
}
|
96
|
+
/* Handle the widths */
|
97
|
+
.masthead,
|
98
|
+
.mastfoot,
|
99
|
+
.cover-container {
|
100
|
+
width: 100%; /* Must be percentage or pixels for horizontal alignment */
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
@media (min-width: 992px) {
|
105
|
+
.masthead,
|
106
|
+
.mastfoot,
|
107
|
+
.cover-container {
|
108
|
+
width: 700px;
|
109
|
+
}
|
110
|
+
}
|
111
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_dependency "minimum_viable_product/controller"
|
2
|
+
|
3
|
+
module MinimumViableProduct
|
4
|
+
class AnalyticsController < Controller
|
5
|
+
def optout
|
6
|
+
cookies[AnalyticsConcern::INVISIBLE_SESSION_COOKIE] = {
|
7
|
+
:value => true,
|
8
|
+
:expires => 1.year.from_now,
|
9
|
+
}
|
10
|
+
|
11
|
+
redirect_to root_path
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module MinimumViableProduct
|
2
|
+
module AnalyticsConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
INVISIBLE_SESSION_COOKIE = 'invisible'
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :generate_uid_if_not_set
|
9
|
+
helper_method :analytics_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def track!(event, properties={})
|
13
|
+
return if session[INVISIBLE_SESSION_COOKIE].to_b == true
|
14
|
+
return unless analytics
|
15
|
+
analytics.track({
|
16
|
+
user_id: analytics_id,
|
17
|
+
event: event,
|
18
|
+
properties: properties.reverse_merge({
|
19
|
+
iteration: MVP::Iteration.version,
|
20
|
+
user: current_user.try(:name),
|
21
|
+
url: request.fullpath
|
22
|
+
})
|
23
|
+
})
|
24
|
+
end
|
25
|
+
|
26
|
+
def slack!(message)
|
27
|
+
if ENV['SLACK_WEBHOOK_URL']
|
28
|
+
begin
|
29
|
+
slack_notifier.ping message
|
30
|
+
rescue
|
31
|
+
slack_notifier.ping "Tried to use slack messaging but failure happened"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def page!(name, opts={})
|
37
|
+
return if sessions[INVISIBLE_SESSION_COOKIE].to_b == true
|
38
|
+
return unless analytics
|
39
|
+
analytics.page(
|
40
|
+
user_id: current_user.try(:id),
|
41
|
+
name: name,
|
42
|
+
category: opts[:category],
|
43
|
+
properties: {
|
44
|
+
url: opts[:url],
|
45
|
+
iteration: MVP::Iteration.version
|
46
|
+
}
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def analytics
|
53
|
+
unless defined?(Analytics)
|
54
|
+
puts ""
|
55
|
+
puts "Please set SEGMENT_WRITE_KEY in .env"
|
56
|
+
puts ""
|
57
|
+
nil
|
58
|
+
else
|
59
|
+
Analytics
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def slack_notifier
|
64
|
+
Slack::Notifier.new ENV['SLACK_WEBHOOK_URL'], channel: ENV['SLACK_WEBHOOK_URL'],
|
65
|
+
username: ENV['SLACK_POST_USERNAME'],
|
66
|
+
channel: ENV['SLACK_POST_CHANNEL']
|
67
|
+
end
|
68
|
+
|
69
|
+
def analytics_id
|
70
|
+
current_user.try(:id) || cookies[:uid]
|
71
|
+
end
|
72
|
+
|
73
|
+
def generate_uid_if_not_set
|
74
|
+
cookies[:uid] = SecureRandom.uuid unless cookies[:uid].present?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module MinimumViableProduct
|
2
|
+
module SEOConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :force_no_trailing_slash
|
9
|
+
helper_method :page
|
10
|
+
end
|
11
|
+
|
12
|
+
def force_no_trailing_slash
|
13
|
+
return if request.path == '/'
|
14
|
+
redirect_to request.original_url.gsub(/\/$/,'') if request.original_url.match(/\/$/)
|
15
|
+
end
|
16
|
+
|
17
|
+
def page
|
18
|
+
@page ||= begin
|
19
|
+
h = Hashie::Mash.new
|
20
|
+
h.classes = []
|
21
|
+
h
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module MinimumViableProduct
|
2
|
+
module SessionConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
COOKIE_ID = '_session'
|
6
|
+
|
7
|
+
included do
|
8
|
+
helper_method :current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
def sign_in(user)
|
12
|
+
session[COOKIE_ID] = user.id
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_user
|
16
|
+
@current_user ||= begin
|
17
|
+
if session[COOKIE_ID]
|
18
|
+
Conversion.find(session[COOKIE_ID])
|
19
|
+
end
|
20
|
+
rescue ActiveRecord::RecordNotFound
|
21
|
+
session[COOKIE_ID] = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# http://blog.laaz.org/tech/2012/12/27/rails-redirect_back_or_default/
|
26
|
+
def store_location
|
27
|
+
session[:return_to] = if request.get?
|
28
|
+
request.url
|
29
|
+
else
|
30
|
+
request.referer
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# http://blog.laaz.org/tech/2012/12/27/rails-redirect_back_or_default/
|
35
|
+
def redirect_back_or_default(default = root_url)
|
36
|
+
redirect_to(session.delete(:return_to) || request.referer || default)
|
37
|
+
end
|
38
|
+
alias_method :redirect_to_back_or_default, :redirect_back_or_default
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'concerns/analytics_concern'
|
2
|
+
require_relative 'concerns/seo_concern'
|
3
|
+
require_relative 'concerns/session_concern'
|
4
|
+
|
5
|
+
module MinimumViableProduct
|
6
|
+
class Controller < ActionController::Base
|
7
|
+
include MinimumViableProduct::AnalyticsConcern
|
8
|
+
include MinimumViableProduct::SEOConcern
|
9
|
+
include MinimumViableProduct::SessionConcern
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_dependency "minimum_viable_product/controller"
|
2
|
+
|
3
|
+
module MinimumViableProduct
|
4
|
+
class SeoController < Controller
|
5
|
+
def robots
|
6
|
+
render text: %%
|
7
|
+
User-agent: *
|
8
|
+
Disallow:
|
9
|
+
Sitemap: #{sitemap_url}
|
10
|
+
%, content_type: "text/plain"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_dependency "minimum_viable_product/controller"
|
2
|
+
|
3
|
+
module MinimumViableProduct
|
4
|
+
class StyleguideController < Controller
|
5
|
+
def layout_basic
|
6
|
+
render template: 'styleguide/layouts/basic', layout: 'basic'
|
7
|
+
end
|
8
|
+
|
9
|
+
def layout_cover
|
10
|
+
render template: 'styleguide/layouts/cover', layout: 'cover'
|
11
|
+
end
|
12
|
+
|
13
|
+
def layout_carousel
|
14
|
+
render template: 'styleguide/layouts/carousel', layout: 'carousel'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MinimumViableProduct
|
2
|
+
module ApplicationHelper
|
3
|
+
def body_id
|
4
|
+
"#{controller_name}-#{action_name}-page"
|
5
|
+
end
|
6
|
+
|
7
|
+
def body_classes
|
8
|
+
klass = []
|
9
|
+
klass << "#{controller_name}-pages"
|
10
|
+
klass << page.classes if page.classes.present?
|
11
|
+
klass << "hidden-header" if page.header == false
|
12
|
+
klass.join(' ')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module MinimumViableProduct
|
2
|
+
module BootstrapHelper
|
3
|
+
def col_groups_of(count, collection, opts={})
|
4
|
+
return if collection.blank?
|
5
|
+
|
6
|
+
html = []
|
7
|
+
collection.in_groups_of(count).each do |groups|
|
8
|
+
groups.compact.each do |obj|
|
9
|
+
html << capture do
|
10
|
+
content_tag :div, class: ["col-md-#{12/count.to_f.floor}", opts[:col_class]].compact.join(' ') do
|
11
|
+
yield obj
|
12
|
+
end
|
13
|
+
end.html_safe
|
14
|
+
end
|
15
|
+
end
|
16
|
+
concat html.join.html_safe
|
17
|
+
end
|
18
|
+
|
19
|
+
def row_groups_of(count, collection, opts={}, &block)
|
20
|
+
return if collection.blank?
|
21
|
+
|
22
|
+
html = []
|
23
|
+
subsetsize = (collection.count/count.to_f)
|
24
|
+
subsetsize.ceil.times do |i|
|
25
|
+
subset = collection[count*i...((count*i)+count)]
|
26
|
+
html << capture do
|
27
|
+
content_tag :div, class: ["row",opts[:row_class]].compact.join(' ') do
|
28
|
+
capture do
|
29
|
+
col_groups_of(count, subset, opts, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end.html_safe
|
33
|
+
end
|
34
|
+
|
35
|
+
concat html.join.html_safe
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Slugification
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def slugify(name, opts={})
|
6
|
+
_opts = opts.reverse_merge(:if => lambda{ |obj| obj.send("#{name}_changed?".to_sym) })
|
7
|
+
before_validation -> { self.slug = slugify(self.send(name.to_sym)) }, _opts
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def slugify(str)
|
14
|
+
str.gsub(/\s/,'_').dasherize.downcase
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<nav class="navbar navbar-default mt20">
|
2
|
+
<div class="container-fluid">
|
3
|
+
<!-- Brand and toggle get grouped for better mobile display -->
|
4
|
+
<div class="navbar-header">
|
5
|
+
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
6
|
+
<span class="sr-only">Toggle navigation</span>
|
7
|
+
<span class="icon-bar"></span>
|
8
|
+
<span class="icon-bar"></span>
|
9
|
+
<span class="icon-bar"></span>
|
10
|
+
</button>
|
11
|
+
<a class="navbar-brand" href="#">__PROJECT_NAME__</a>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<!-- Collect the nav links, forms, and other content for toggling -->
|
15
|
+
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
16
|
+
<ul class="nav navbar-nav">
|
17
|
+
<li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
|
18
|
+
<li><a href="#">Link</a></li>
|
19
|
+
<li class="dropdown">
|
20
|
+
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
|
21
|
+
<ul class="dropdown-menu">
|
22
|
+
<li><a href="#">Action</a></li>
|
23
|
+
<li><a href="#">Another action</a></li>
|
24
|
+
<li><a href="#">Something else here</a></li>
|
25
|
+
<li role="separator" class="divider"></li>
|
26
|
+
<li><a href="#">Separated link</a></li>
|
27
|
+
<li role="separator" class="divider"></li>
|
28
|
+
<li><a href="#">One more separated link</a></li>
|
29
|
+
</ul>
|
30
|
+
</li>
|
31
|
+
</ul>
|
32
|
+
|
33
|
+
<ul class="nav navbar-nav navbar-right">
|
34
|
+
<li><a href="#">Link</a></li>
|
35
|
+
<li class="dropdown">
|
36
|
+
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
|
37
|
+
<ul class="dropdown-menu">
|
38
|
+
<li><a href="#">Action</a></li>
|
39
|
+
<li><a href="#">Another action</a></li>
|
40
|
+
<li><a href="#">Something else here</a></li>
|
41
|
+
<li role="separator" class="divider"></li>
|
42
|
+
<li><a href="#">Separated link</a></li>
|
43
|
+
</ul>
|
44
|
+
</li>
|
45
|
+
</ul>
|
46
|
+
</div><!-- /.navbar-collapse -->
|
47
|
+
</div><!-- /.container-fluid -->
|
48
|
+
</nav>
|