minimum-viable-product 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +37 -0
- data/app/assets/javascripts/mvp/application.js.erb +38 -0
- data/app/assets/javascripts/mvp/init/controllers.js +23 -0
- data/app/assets/stylesheets/mvp/application.scss +23 -0
- data/app/assets/stylesheets/mvp/components/buttons.scss +20 -0
- data/app/assets/stylesheets/mvp/components/fonts.scss +15 -0
- data/app/assets/stylesheets/mvp/components/spacing.scss +13 -0
- data/app/assets/stylesheets/mvp/components/typography.scss +40 -0
- data/app/assets/stylesheets/mvp/init/_reset.scss +43 -0
- data/app/assets/stylesheets/mvp/init/layout.scss +42 -0
- data/app/assets/stylesheets/mvp/layout/basic.scss +6 -0
- data/app/assets/stylesheets/mvp/layout/carousel.scss +128 -0
- data/app/assets/stylesheets/mvp/layout/cover.scss +111 -0
- data/app/assets/stylesheets/mvp/pages/sample.scss +14 -0
- data/app/assets/stylesheets/mvp/skin/style.scss +8 -0
- data/app/controllers/mvp/analytics_controller.rb +14 -0
- data/app/controllers/mvp/application_controller.rb +11 -0
- data/app/controllers/mvp/concerns/analytics_concern.rb +77 -0
- data/app/controllers/mvp/concerns/seo_concern.rb +25 -0
- data/app/controllers/mvp/concerns/session_concern.rb +40 -0
- data/app/controllers/mvp/seo_controller.rb +13 -0
- data/app/controllers/mvp/styleguide_controller.rb +17 -0
- data/app/helpers/mvp/analytics_helper.rb +4 -0
- data/app/helpers/mvp/application_helper.rb +15 -0
- data/app/helpers/mvp/bootstrap_helper.rb +38 -0
- data/app/helpers/mvp/seo_helper.rb +4 -0
- data/app/helpers/mvp/styleguide_helper.rb +4 -0
- data/app/models/concerns/slugification.rb +16 -0
- data/app/views/layouts/mvp/_header.html.erb +48 -0
- data/app/views/layouts/mvp/_instrumentation.html.erb +75 -0
- data/app/views/layouts/mvp/_meta.html.erb +37 -0
- data/app/views/layouts/mvp/application.html.erb +24 -0
- data/app/views/layouts/mvp/basic.html.erb +10 -0
- data/app/views/layouts/mvp/carousel.html.erb +13 -0
- data/app/views/layouts/mvp/cover.html.erb +13 -0
- data/bin/mvp +23 -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/mvp.rb +6 -0
- data/lib/mvp/engine.rb +20 -0
- data/lib/mvp/ext/nil.rb +5 -0
- data/lib/mvp/ext/string.rb +7 -0
- data/lib/mvp/version.rb +3 -0
- data/lib/tasks/sitemap.rake +42 -0
- metadata +521 -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 "mvp/application_controller"
|
2
|
+
|
3
|
+
module Mvp
|
4
|
+
class AnalyticsController < ApplicationController
|
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,11 @@
|
|
1
|
+
require_relative 'concerns/analytics_concern'
|
2
|
+
require_relative 'concerns/seo_concern'
|
3
|
+
require_relative 'concerns/session_concern'
|
4
|
+
|
5
|
+
module Mvp
|
6
|
+
class ApplicationController < ActionController::Base
|
7
|
+
include Mvp::AnalyticsConcern
|
8
|
+
include Mvp::SEOConcern
|
9
|
+
include Mvp::SessionConcern
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Mvp
|
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 Mvp
|
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 Mvp
|
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,17 @@
|
|
1
|
+
require_dependency "mvp/application_controller"
|
2
|
+
|
3
|
+
module Mvp
|
4
|
+
class StyleguideController < ApplicationController
|
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 Mvp
|
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 Mvp
|
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>
|