diff --git a/Guardfile b/Guardfile index fbbeaf8e1..14dac7561 100644 --- a/Guardfile +++ b/Guardfile @@ -65,7 +65,7 @@ guard :rspec, rspec_options do [ rspec.spec.call("routing/#{m[1]}_routing"), rspec.spec.call("controllers/#{m[1]}_controller"), - rspec.spec.call("acceptance/#{m[1]}"), + rspec.spec.call("acceptance/#{m[1]}") ] end diff --git a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb index 5aa7ac93e..85cdd275f 100644 --- a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb +++ b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb @@ -12,9 +12,6 @@ function initializeBaseTracking() { wait++; if (window.ga && ga.create) { ga('create', '<%= ApplicationConfig["GA_TRACKING_ID"] %>', 'auto'); - if (!checkUserLoggedIn() && Math.random() < 0.35) { - ga('require', '<%= ApplicationConfig["GA_OPTIMIZE_ID"] %>'); - } ga('send', 'pageview', location.pathname + location.search); if (document.location.search.indexOf('newly-registered-user=true') > -1) { ga('send', 'event', 'registration', 'New user registration', location.pathname, null); @@ -29,6 +26,7 @@ function initializeBaseTracking() { }, 25); facebookTrackingScript(); eventListening(); + trackCustomImpressions(); } function fallbackActivityRecording() { @@ -107,4 +105,50 @@ function logImpressions() { var data = el.dataset.featuredArticle; ga('send', 'event', 'view', 'featured-feed-impression', data, null); } +} + +function trackCustomImpressions() { + setTimeout(function(){ + var stickyNav = document.getElementById('article-show-primary-sticky-nav'); + var sidebarHTMLVariant = document.getElementById('html-variant-article-show-sidebar'); + var ArticleElement = document.getElementById('article-body'); + var tokenMeta = document.querySelector("meta[name='csrf-token']") + var randomNumber = Math.floor(Math.random() * 10); // 1 in 10; Only track 1 in 10 impressions + var isBot = /bot|google|baidu|bing|msn|duckduckbot|teoma|slurp|yandex/i.test(navigator.userAgent) // is crawler + if (sidebarHTMLVariant && ArticleElement && tokenMeta && !isBot) { + var dataBody = { + html_variant_id: sidebarHTMLVariant.dataset.variantId, + article_id: ArticleElement.dataset.articleId, + }; + var csrfToken = tokenMeta.getAttribute('content'); + if (randomNumber === 1) { + window.fetch('/html_variant_trials', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dataBody), + credentials: 'same-origin', + }); + } + var successLinks = stickyNav.querySelectorAll('a,button'); //track all links and button clicks within nav + for(var i = 0; i < successLinks.length; i++) + { + successLinks[i].addEventListener('click', function() { trackHtmlVariantSuccess(dataBody, csrfToken) }); + } + } + }, 1500) +} + +function trackHtmlVariantSuccess(dataBody, csrfToken) { + window.fetch('/html_variant_successes', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dataBody), + credentials: 'same-origin', + }) } \ No newline at end of file diff --git a/app/assets/stylesheets/article-show.scss b/app/assets/stylesheets/article-show.scss index d273e35ff..b5f20a03b 100644 --- a/app/assets/stylesheets/article-show.scss +++ b/app/assets/stylesheets/article-show.scss @@ -663,7 +663,7 @@ header{ button{ background:transparent; margin: 0px 0.22vw; - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); + padding: 3px calc(0.88vw + 3px); border-radius:100px; border:2px solid darken($light-gray, 7%); font-family: $helvetica-condensed; @@ -702,7 +702,6 @@ header{ } &.user-activated { border:2px solid darken($bold-blue, 40%); - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); background: linear-gradient(60deg, #f4d2d2, #efb6b6); img{ filter: none; @@ -1004,13 +1003,15 @@ header{ @keyframes reaction { 0% { - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); + padding-left: calc(0.88vw + 3px); + padding-right: calc(0.88vw + 3px); } 50% { - padding:calc(0.05vw + 3px) calc(1.1vw + 3px); - margin: 0px 0.00vw; + padding-left: calc(1.1vw + 3px); + padding-left: calc(1.1vw + 3px); } 100% { - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); + padding-left: calc(0.88vw + 3px); + padding-right: calc(0.88vw + 3px); } } \ No newline at end of file diff --git a/app/assets/stylesheets/settings.scss b/app/assets/stylesheets/settings.scss index 33c511d2a..dc51df73c 100644 --- a/app/assets/stylesheets/settings.scss +++ b/app/assets/stylesheets/settings.scss @@ -108,7 +108,6 @@ } } label { - width: 160px; display:inline-block; vertical-align:top; padding:14px 0px 8px; diff --git a/app/assets/stylesheets/sticky-nav.scss b/app/assets/stylesheets/sticky-nav.scss index f2fba9719..641020aa3 100644 --- a/app/assets/stylesheets/sticky-nav.scss +++ b/app/assets/stylesheets/sticky-nav.scss @@ -39,13 +39,12 @@ .primary-sticky-nav-author-follow{ padding-top:8px; button{ - width:136px; - font-size:1.1em; + width:100%; + font-size:1.25em; border:0px; border-radius:3px; padding: 3px; - height: 29px; - + height: 44px; } } } @@ -112,38 +111,67 @@ box-shadow: 3px 4px 0px darken($light-medium-gray, 17%); } } + .user-metadata-details { + margin-top: 10px; + margin-left: 2px; + font-family: $monospace; + .row{ + padding: calc(5px + 0.2vw) 0px; + display: inline-block; + width: 96%; + } + .key{ + font-size: 0.8em; + font-weight: 800; + margin-bottom: 2px; + color: $medium-gray; + } + .value{ + display: inline-block; + font-size: 0.92em; + margin-bottom: 4px; + } + } + .html-variant-wrapper{ + margin: 10px; + } } -.primary-sticky-nav-org{ - position:relative; - .sticky-box-connector{ - position:absolute; - top: - 10px; - height: 10px; - width: 5px; - background: $outline-color; - left: 15px; - &.sticky-box-right-connector{ - left: auto; - right: 15px; - } - } + .primary-sticky-nav-org-summary{ font-weight: 400; - color: $medium-gray; - padding: 0px 0px 5px; - font-size:0.88em; + color: $black; + padding: 4px 0px; + font-size:0.95em; + margin-top: 20px; + border-top: 1px solid $light-medium-gray; p{ - margin:5px 0px; + margin:12px 0px; + } + a.primary-sticky-nav-org-cta-link{ + width:100%; + display: block; + font-size:1.25em; + border:0px; + border-radius:3px; + padding: 8px 0px 3px; + margin-top: 20px; + background: white; + color: $black; + font-family: $helvetica-condensed; + border: 1px solid darken($light-medium-gray, 7%); + text-align: center; + .primary-sticky-nav-org-cta-link-domain-small{ + display: block; + padding-top: 2px; + font-size:0.8em; + color: darken($light-medium-gray, 4%); + font-family: $monospace; + } + &:hover { + border: 1px solid darken($light-medium-gray, 12%); + box-shadow: $shadow; + transition: all 0.1s; + } } } - button{ - min-width:135px; - font-size:1.1em; - border:0px; - border-radius:3px; - padding: 3px; - display: block; - margin-top:18px; - } -} diff --git a/app/black_box/google_analytics.rb b/app/black_box/google_analytics.rb index dcb4a8ea4..2c88af89b 100644 --- a/app/black_box/google_analytics.rb +++ b/app/black_box/google_analytics.rb @@ -61,12 +61,10 @@ class GoogleAnalytics metrics: [Metric.new(expression: metrics_string)], dimensions: [Dimension.new(name: "ga:segment")], segments: [Segment.new(segment_id: "gaid::-1"), - Segment.new(segment_id: "gaid::-2"), - Segment.new(segment_id: "gaid::-19"), - Segment.new(segment_id: "gaid::-7")], - date_ranges: [ - DateRange.new(start_date: @start_date, end_date: "2020-01-01"), - ], + Segment.new(segment_id: "gaid::-2"), + Segment.new(segment_id: "gaid::-19"), + Segment.new(segment_id: "gaid::-7")], + date_ranges: [DateRange.new(start_date: @start_date, end_date: "2020-01-01")], ) end diff --git a/app/controllers/admin/html_variants_controller.rb b/app/controllers/admin/html_variants_controller.rb new file mode 100644 index 000000000..0ca6e91f7 --- /dev/null +++ b/app/controllers/admin/html_variants_controller.rb @@ -0,0 +1,21 @@ +module Admin + class HtmlVariantsController < Admin::ApplicationController + # To customize the behavior of this controller, + # you can overwrite any of the RESTful actions. For example: + # + # def index + # super + # @resources = HtmlVariant. + # page(params[:page]). + # per(10) + # end + + # Define a custom finder by overriding the `find_resource` method: + # def find_resource(param) + # HtmlVariant.find_by!(slug: param) + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/html_variant_successes_controller.rb b/app/controllers/html_variant_successes_controller.rb new file mode 100644 index 000000000..38b267b39 --- /dev/null +++ b/app/controllers/html_variant_successes_controller.rb @@ -0,0 +1,6 @@ +class HtmlVariantSuccessesController < ApplicationController + def create + HtmlVariantSuccess.delay.create(html_variant_id: params[:html_variant_id], article_id: params[:article_id]) + head :ok + end +end diff --git a/app/controllers/html_variant_trials_controller.rb b/app/controllers/html_variant_trials_controller.rb new file mode 100644 index 000000000..00d212136 --- /dev/null +++ b/app/controllers/html_variant_trials_controller.rb @@ -0,0 +1,6 @@ +class HtmlVariantTrialsController < ApplicationController + def create + HtmlVariantTrial.delay.create(html_variant_id: params[:html_variant_id], article_id: params[:article_id]) + head :ok + end +end diff --git a/app/controllers/html_variants_controller.rb b/app/controllers/html_variants_controller.rb new file mode 100644 index 000000000..45dc1b5b7 --- /dev/null +++ b/app/controllers/html_variants_controller.rb @@ -0,0 +1,53 @@ +class HtmlVariantsController < ApplicationController + after_action :verify_authorized + + def index + authorize HtmlVariant + @user_variants = current_user.html_variants.order("created_at DESC") + @all_published_variants = HtmlVariant.where(published: true).order("created_at DESC") + @preview_path = Article.where(featured: true, published: true).order("published_at DESC").first&.path.to_s + end + + def new + authorize HtmlVariant + @html_variant = HtmlVariant.new + if params[:fork_id] + @fork = HtmlVariant.find(params[:fork_id]) + @html_variant.name = @fork.name + " FORK" + @html_variant.html = @fork.html + end + end + + def edit + @html_variant = HtmlVariant.find(params[:id]) + authorize @html_variant + end + + def create + authorize HtmlVariant + @html_variant = HtmlVariant.new(html_variant_params) + @html_variant.group = "article_show_sidebar_cta" + @html_variant.user_id = current_user.id + if @html_variant.save + redirect_to "/html_variants" + else + render :new + end + end + + def update + @html_variant = HtmlVariant.find(params[:id]) + authorize @html_variant + if @html_variant.update(html_variant_params) + redirect_to "/html_variants" + else + render :edit + end + end + + private + + def html_variant_params + params.require(:html_variant).permit(policy(HtmlVariant).permitted_attributes) + end +end diff --git a/app/dashboards/dashboard_manifest.rb b/app/dashboards/dashboard_manifest.rb index 37bb7fe12..50da85be5 100644 --- a/app/dashboards/dashboard_manifest.rb +++ b/app/dashboards/dashboard_manifest.rb @@ -25,6 +25,7 @@ class DashboardManifest display_ads badges badge_achievements + html_variants ].freeze # DASHBOARDS = [ # :users, diff --git a/app/dashboards/html_variant_dashboard.rb b/app/dashboards/html_variant_dashboard.rb new file mode 100644 index 000000000..fcc37fa1c --- /dev/null +++ b/app/dashboards/html_variant_dashboard.rb @@ -0,0 +1,78 @@ +require "administrate/base_dashboard" + +class HtmlVariantDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + user: Field::BelongsTo, + html_variant_trials: Field::HasMany, + html_variant_successes: Field::HasMany, + id: Field::Number, + group: Field::String, + name: Field::String, + html: Field::Text, + target_tag: Field::String, + success_rate: Field::Number.with_options(decimals: 2), + published: Field::Boolean, + approved: Field::Boolean, + created_at: Field::DateTime, + updated_at: Field::DateTime, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = [ + :user, + :html_variant_trials, + :html_variant_successes, + :id, + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = [ + :user, + :html_variant_trials, + :html_variant_successes, + :id, + :group, + :name, + :html, + :target_tag, + :success_rate, + :published, + :approved, + :created_at, + :updated_at, + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = [ + :user, + :html_variant_trials, + :html_variant_successes, + :group, + :name, + :html, + :target_tag, + :success_rate, + :published, + :approved, + ].freeze + + # Overwrite this method to customize how html variants are displayed + # across all pages of the admin dashboard. + # + # def display_resource(html_variant) + # "HtmlVariant ##{html_variant.id}" + # end +end diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index fdbcad935..7bad166c2 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -44,8 +44,8 @@ class UserDecorator < ApplicationDecorator text: "#ffffff" }, { - bg: "#edebf6", - text: " #070126" + bg: "#2e0338", + text: " #ffffff" }, { bg: "#080E3B", diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 47f86110c..bd2787b66 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -29,6 +29,7 @@ module ApplicationHelper comments notifications reading_list_items + html_variants ).include?(controller_name) end @@ -153,7 +154,7 @@ module ApplicationHelper end def follow_button(followable, style = "full") - tag :button, + tag :button, #Yikes class: "cta follow-action-button", data: { info: { id: followable.id, className: followable.class.name, style: style }.to_json, diff --git a/app/liquid_tags/user_tag.rb b/app/liquid_tags/user_tag.rb index a21b58ef7..34964f2bc 100644 --- a/app/liquid_tags/user_tag.rb +++ b/app/liquid_tags/user_tag.rb @@ -1,5 +1,6 @@ class UserTag < LiquidTagBase include ApplicationHelper + include ActionView::Helpers::TagHelper attr_reader :user def initialize(_tag_name, user, _tokens) diff --git a/app/models/html_variant.rb b/app/models/html_variant.rb new file mode 100644 index 000000000..86f6134e9 --- /dev/null +++ b/app/models/html_variant.rb @@ -0,0 +1,40 @@ +class HtmlVariant < ApplicationRecord + validates :html, presence: true + validates :name, uniqueness: true + validates :group, inclusion: { in: %w(article_show_sidebar_cta) } + validates :success_rate, presence: true + validate :no_edits + belongs_to :user, optional: true + has_many :html_variant_trials + has_many :html_variant_successes + + def calculate_success_rate! + self.success_rate = html_variant_successes.size.to_f / (html_variant_trials.size * 10.0) # x10 because we only capture every 10th + save! + end + + def self.find_for_test(tags = []) + tags_array = tags + ["", nil] + if rand(10) == 1 # 10% return completely random + find_random_for_test(tags_array) + else # 90% chance return one in top 10 + find_top_for_test(tags_array) + end + end + + def self.find_top_for_test(tags_array) + where(group: "article_show_sidebar_cta", approved: true, published: true, target_tag: tags_array).order("success_rate DESC").limit(rand(1..10)).sample + end + + def self.find_random_for_test(tags_array) + where(group: "article_show_sidebar_cta", approved: true, published: true, target_tag: tags_array).order("RANDOM()").first + end + + private + + def no_edits + if (approved && html_changed? || name_changed? || group_changed?) && persisted? + errors.add(:base, "cannot change once published and approved") + end + end +end diff --git a/app/models/html_variant_success.rb b/app/models/html_variant_success.rb new file mode 100644 index 000000000..ef2d2d4aa --- /dev/null +++ b/app/models/html_variant_success.rb @@ -0,0 +1,5 @@ +class HtmlVariantSuccess < ApplicationRecord + validates :html_variant_id, presence: true + belongs_to :html_variant + belongs_to :article, optional: true +end diff --git a/app/models/html_variant_trial.rb b/app/models/html_variant_trial.rb new file mode 100644 index 000000000..3502bfcf5 --- /dev/null +++ b/app/models/html_variant_trial.rb @@ -0,0 +1,5 @@ +class HtmlVariantTrial < ApplicationRecord + validates :html_variant_id, presence: true + belongs_to :html_variant + belongs_to :article, optional: true +end diff --git a/app/models/organization.rb b/app/models/organization.rb index 227601dc8..11bf5ed09 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -36,7 +36,7 @@ class Organization < ApplicationRecord validates :cta_button_url, url: { allow_blank: true, no_local: true, schemes: ["https", "http"] }, if: :approved validates :cta_button_text, length: { maximum: 12 } - validates :cta_body_markdown, length: { maximum: 140 } + validates :cta_body_markdown, length: { maximum: 256 } before_save :remove_at_from_usernames after_save :bust_cache before_save :generate_secret diff --git a/app/models/user.rb b/app/models/user.rb index 2e3c0f988..7e4e9f81f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,6 +32,7 @@ class User < ApplicationRecord has_many :chat_channels, through: :chat_channel_memberships has_many :push_notification_subscriptions, dependent: :destroy has_many :feedback_messages + has_many :html_variants, dependent: :destroy has_many :mentor_relationships_as_mentee, class_name: "MentorRelationship", foreign_key: "mentee_id" has_many :mentor_relationships_as_mentor, diff --git a/app/policies/html_variant_policy.rb b/app/policies/html_variant_policy.rb new file mode 100644 index 000000000..d7d705087 --- /dev/null +++ b/app/policies/html_variant_policy.rb @@ -0,0 +1,33 @@ +class HtmlVariantPolicy < ApplicationPolicy + def index? + user_admin? + end + + def show? + user_admin? + end + + def edit? + user_admin? + end + + def update? + user_admin? + end + + def new? + user_admin? + end + + def create? + user_admin? + end + + def destroy? + user_admin? + end + + def permitted_attributes + %i[html name published approved target_tag] + end +end diff --git a/app/views/articles/_sticky_nav.html.erb b/app/views/articles/_sticky_nav.html.erb index 92254b129..ca538ba55 100644 --- a/app/views/articles/_sticky_nav.html.erb +++ b/app/views/articles/_sticky_nav.html.erb @@ -1,6 +1,8 @@ -<% @sticky_collection = StickyArticleCollection.new(@article, @organization || @user) %> -<% @sticky_articles = @sticky_collection.suggested_stickies %> -<% @user_stickies = @sticky_collection.user_stickies %> +<% if user_signed_in? %> + <% @sticky_collection = StickyArticleCollection.new(@article, @organization || @user) %> + <% @sticky_articles = @sticky_collection.suggested_stickies %> + <% @user_stickies = @sticky_collection.user_stickies %> +<% end %> <% @actor = @article.organization || @article.user %> -
You'll get an email when this process finishes. -
+This is an ordinary unpublished post. Add title/tags/body/etc, and then mark as published after the video is done processing.
diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb index 4f19ce35b..38ff1a1d6 100644 --- a/app/views/articles/show.html.erb +++ b/app/views/articles/show.html.erb @@ -216,7 +216,7 @@