Self-serve sponsorship options (#3371)

* WIP - Initial work on sponsorship landing page

* Add initial partnership page
This commit is contained in:
Ben Halpern 2019-07-03 12:04:03 -04:00 committed by GitHub
parent 22e76bee84
commit 4ebfcd8bc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 952 additions and 29 deletions

View file

@ -1121,6 +1121,23 @@
}
}
}
.sidebar-sponsorship-level {
text-align: center;
font-weight: bold;
width: calc(100% - 6px);
padding: 26px 0px;
border-radius: 3px;
margin-bottom: 10px;
font-family: $helvetica-condensed;
font-stretch: condensed;
font-size: 1.1em;
@include themeable(
border,
theme-container-border,
1px solid $outline-color
);
@include themeable(box-shadow, theme-container-box-shadow, $bold-shadow);
}
.widget {
width: calc(100% - 30px);
padding: 10px 12px;

View file

@ -0,0 +1,192 @@
@import 'variables';
@import 'mixins';
.partnerships-container {
width: 800px;
max-width: 96%;
margin: 100px auto;
&.partnerships-container-show {
h1 {
text-align: left;
}
}
h1 {
text-align: center;
font-size: 2.5em;
padding: 1em 0;
img {
height: 2.2em;
width: 2.2em;
transform: rotate(6deg);
border-radius: 0.2em;
vertical-align: -0.7em;
margin-left: 0.3em;
}
}
.partner-credits-explainer {
@include themeable(
background,
theme-container-background,
$white
);
box-sizing: border-box;
padding: 25px 20px 30px;
border-radius: 12px;
margin: 50px auto 40px;
width: calc(100% - 20px);
@include themeable(
border,
theme-border,
1px solid $light-medium-gray
);
@include themeable(
box-shadow,
theme-container-box-shadow,
2px 2px 8px darken($light-medium-gray, 5%)
);
.partner-explainer-notice {
padding: 15px;
margin: 25px 0px 12px;
border-radius: 3px;
font-weight: bold;
@include themeable(
background,
theme-container-accent-background,
$light-gray
);
}
h4 {
font-size: 0.8em;
margin: 3px 0px;
@include themeable(
color,
theme-secondary-color,
$medium-gray
);
}
h3 {
margin: 5px 0px;
font-size: 1.8em;
@include themeable(
color,
theme-color,
#131a38
);
img {
height: 1.2em;
width: 1.2em;
vertical-align: -0.25em;
border-radius: 100px;
margin-right: 0.3em;
}
}
select {
display: block;
font-size: 1.3em;
}
textarea {
width: 80%;
font-size: 1em;
padding: 0.5em;
border-radius: 3px;
display: block;
height: 300px;
@include themeable(
border,
theme-container-border,
1px solid $light-medium-gray
);
}
input {
width: 50%;
font-size: 1.3em;
padding: 0.5em;
border-radius: 3px;
display: block;
@include themeable(
border,
theme-container-border,
1px solid $light-medium-gray
);
}
a, button {
padding: 5px 20px;
font-size: 1.5em;
border-radius: 8px;
font-weight: bold;
margin: 8px 0px;
display: inline-block;
border: 0px;
@include themeable(
background,
theme-anchor-color,
$bold-blue
);
@include themeable(
color,
theme-container-background,
white
);
}
}
.partnership-options {
margin: 40px 0px;
}
.partnership-option-card {
@include themeable(
background,
theme-container-background,
$white
);
text-align: left;
width: 100%;
@media screen and ( min-width: 820px ){
width: calc(50% - 15px);
margin-right: 8px;
ul {
height: 220px;
}
}
box-sizing: border-box;
padding: 15px;
border-radius: 12px;
margin: 10px 0px;
display: inline-block;
@include themeable(
border,
theme-border,
1px solid $light-medium-gray
);
@include themeable(
box-shadow,
theme-container-box-shadow,
2px 2px 8px darken($light-medium-gray, 5%)
);
li {
padding: 8px 0px;
}
h2 {
margin: 0px;
padding: 10px 0px;
border-bottom: 2px solid rgba(0,0,0,0.1);
}
.partnership-option-cta {
display: block;
font-size: 1.6em;
padding: 20px 0px;
text-align: center;
border-radius: 5px;
font-weight: bold;
@include themeable(
background,
theme-anchor-color,
$bold-blue
);
@include themeable(
color,
theme-container-background,
white
);
}
}
}

View file

@ -0,0 +1,104 @@
class PartnershipsController < ApplicationController
before_action :set_cache_control_headers, only: %i[index]
before_action :authenticate_user!, only: %i[create]
after_action :verify_authorized
def index
skip_authorization
set_surrogate_key_header "partnership-index"
end
def show
skip_authorization
end
def create
@level = params[:sponsorship_level]
@number_of_credits_needed = credits_for_level
@organization = Organization.find(params[:organization_id])
update_sponsorship_instructions
authorize @organization, :admin_of_org?
@available_org_credits = @organization.credits.where(spent: false)
if @level == "tag"
@tag = Tag.find_by(name: params[:tag_name])
@tag.sponsor_organization_id = @organization.id
slackbot_ping("@#{current_user.username} bought a ##{@tag.name} sponsorship for @#{@organization.username}")
if @available_org_credits.size >= @number_of_credits_needed
@tag.save!
spend_credits
redirect_back(fallback_location: "/partnerships")
else
raise "Not enough credits"
end
elsif @level == "media"
# For now. Just ping slack.
slackbot_ping("📹 @#{current_user.username} bought #{@number_of_credits_needed} media credits for @#{@organization.username}")
if @available_org_credits.size >= @number_of_credits_needed
spend_credits
redirect_back(fallback_location: "/partnerships")
end
elsif @level == "editorial"
SlackBot.ping(
message: "@#{current_user.username} bought #{@number_of_credits_needed} credits for @#{@organization.username}",
channel: "incoming-partners",
username: "media_sponsor",
icon_emoji: ":partyparrot:",
)
slackbot_ping("✍ @#{current_user.username} bought an editorial partnership for @#{@organization.username}")
if @available_org_credits.size >= @number_of_credits_needed
spend_credits
redirect_back(fallback_location: "/partnerships")
end
else
@organization.sponsorship_level = @level
@organization.sponsorship_status = "pending"
@organization.sponsorship_expires_at = (@organization.sponsorship_expires_at || Time.current) + 1.month
slackbot_ping("@#{current_user.username} bought a #{@level} sponsorship for @#{@organization.username}")
if @available_org_credits.size >= @number_of_credits_needed
@organization.save!
spend_credits
redirect_back(fallback_location: "/partnerships")
else
raise "Not enough credits"
end
end
end
private
def credits_for_level
if @level == "gold"
6000
elsif @level == "silver"
300
elsif @level == "tag"
500
elsif @level == "bronze"
50
elsif @level == "editorial"
500
elsif @level == "media"
params[:sponsorship_amount].to_i
else
raise "Invalid level"
end
end
def update_sponsorship_instructions
@organization.sponsorship_instructions = @organization.sponsorship_instructions + "\n---\n#{Time.current}\n---\n" + params[:sponsorship_instructions].to_s
@organization.sponsorship_instructions_updated_at = Time.current
end
def spend_credits
@available_org_credits.limit(@number_of_credits_needed).update_all(spent: true)
end
def slackbot_ping(text)
SlackBot.ping(
message: text,
channel: "incoming-partners",
username: "media_sponsor",
icon_emoji: ":partyparrot:",
)
end
end

View file

@ -37,7 +37,9 @@ class OrganizationDashboard < Administrate::BaseDashboard
cta_body_markdown: Field::Text,
sponsorship_url: Field::Text,
sponsorship_tagline: Field::Text,
is_gold_sponsor: Field::Boolean,
sponsorship_status: Field::Text,
sponsorship_level: Field::Text,
sponsorship_instructions: Field::Text,
sponsorship_blurb_html: Field::Text,
sponsorship_featured_number: Field::Number
}.freeze
@ -89,7 +91,8 @@ class OrganizationDashboard < Administrate::BaseDashboard
sponsorship_url
sponsorship_tagline
sponsorship_blurb_html
is_gold_sponsor
sponsorship_level
sponsorship_status
sponsorship_featured_number
].freeze

View file

@ -22,6 +22,8 @@ class TagDashboard < Administrate::BaseDashboard
pretty_name: Field::String,
profile_image: CarrierwaveField,
social_image: CarrierwaveField,
sponsorship_organization_id: Field::Number,
sponsorship_status: Field::String,
bg_color_hex: Field::String,
text_color_hex: Field::String,
keywords_for_search: Field::String,
@ -81,6 +83,8 @@ class TagDashboard < Administrate::BaseDashboard
social_image
bg_color_hex
text_color_hex
sponsorship_organization_id
sponsorship_status
keywords_for_search
buffer_profile_id_code
].freeze

View file

@ -25,4 +25,11 @@ class OrganizationDecorator < ApplicationDecorator
text: "#ffffff"
}
end
def sponsorship_color_hex
hexes = { "gold" => "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);",
"silver" => "linear-gradient(to right, #e3e3e3 8%, #f0eded 18%, #e8e8e8 33%);",
"bronze" => "linear-gradient(to right, #ebe2d3 8%, #f5eee1 18%, #ede6d8 33%);" }
hexes[sponsorship_level]
end
end

View file

@ -36,6 +36,7 @@ module ApplicationHelper
html_variants
classified_listings
credits
partnerships
].include?(controller_name)
end
@ -193,4 +194,20 @@ module ApplicationHelper
def community_qualified_name
"The #{ApplicationConfig['COMMUNITY_NAME']} Community"
end
def sponsorship_credits_price(level)
if level == "bronze"
50
elsif level == "silver"
300
elsif level == "gold"
4000
elsif level == "tag"
500
elsif level == "media"
25
elsif level == "editorial"
500
end
end
end

View file

@ -26,6 +26,7 @@ class Organization < ApplicationRecord
validates :jobs_email, email: true, allow_blank: true
validates :text_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
validates :bg_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
validates :sponsorship_level, inclusion: { in: %w[gold silver bronze] }, allow_nil: true
validates :slug,
presence: true,
uniqueness: { case_sensitive: false },
@ -40,6 +41,7 @@ class Organization < ApplicationRecord
message: "Integer only. No sign allowed.",
allow_blank: true }
validates :tech_stack, :story, length: { maximum: 640 }
validates :sponsorship_status, inclusion: { in: %w[none pending live] }
validates :cta_button_url,
url: { allow_blank: true, no_local: true, schemes: %w[https http] }
validates :cta_button_text, length: { maximum: 20 }

View file

@ -18,6 +18,7 @@ class Tag < ActsAsTaggableOn::Tag
attr_accessor :tag_moderator_id, :remove_moderator_id
belongs_to :badge, optional: true
belongs_to :sponsor_organization, class_name: "Organization", optional: true
mount_uploader :profile_image, ProfileImageUploader
mount_uploader :social_image, ProfileImageUploader
@ -27,6 +28,7 @@ class Tag < ActsAsTaggableOn::Tag
validates :bg_color_hex,
format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_nil: true
validates :category, inclusion: { in: ALLOWED_CATEGORIES }
validates :sponsorship_status, inclusion: { in: %w[none pending live] }
validate :validate_alias
before_validation :evaluate_markdown

View file

@ -17,6 +17,12 @@ class OrganizationPolicy < ApplicationPolicy
OrganizationMembership.exists?(user_id: user.id, organization_id: record.id)
end
def admin_of_org?
return false if record.blank?
OrganizationMembership.exists?(user_id: user.id, organization_id: record.id, type_of_user: "admin")
end
def generate_new_secret?
update?
end

View file

@ -10,22 +10,11 @@
<img src="<%= asset_path "emoji/emoji-one-heart.png" %>" alt="emoji heart" />
</span>
</header>
<% @sponsors = Organization.where(is_gold_sponsor: true).order("sponsorship_featured_number ASC") %>
<% @sponsors = Organization.where(sponsorship_level: "gold", sponsorship_status: "live").order("sponsorship_featured_number ASC") %>
<% if @sponsors.any? %>
<div class="widget-body">
<% @sponsors.each do |organization| %>
<div class="sidebar-sponsor">
<a class="partner-link" href="<%= organization.sponsorship_url %>" target="_blank" data-details="<%= organization.slug %>__image">
<img src="<%= cloudinary(organization.nav_image_url) %>" style="margin-bottom:8px;" alt="<%= organization.name %> logo" loading="lazy" class="partner-image-light-mode" />
<img src="<%= cloudinary(organization.dark_nav_image_url || organization.nav_image_url) %>" style="margin-bottom:8px;" alt="<%= organization.name %> logo" loading="lazy" class="partner-image-dark-mode" />
</a>
<div class="sponsor-tagline" style="display:inline;">
<%= organization.sponsorship_tagline %>
<a class="partner-link sponsor-learn-more" href="<%= organization.sponsorship_url %>" target="_blank" data-details="<%= organization.slug %>__learn-more">
Learn More
</a>
</div>
</div>
<%= render "articles/single_sponsor", organization: organization %>
<% end %>
<div class="sponsors-love-message">
We are grateful for wonderful sponsors who help sustain the <%= ApplicationConfig["COMMUNITY_NAME"] %> community.

View file

@ -0,0 +1,12 @@
<div class="sidebar-sponsor">
<a class="partner-link" href="<%= organization.sponsorship_url %>" target="_blank" data-details="<%= organization.slug %>__image">
<img src="<%= cloudinary(organization.nav_image_url) %>" style="margin-bottom:8px;" alt="<%= organization.name %> logo" loading="lazy" class="partner-image-light-mode" />
<img src="<%= cloudinary(organization.dark_nav_image_url || organization.nav_image_url) %>" style="margin-bottom:8px;" alt="<%= organization.name %> logo" loading="lazy" class="partner-image-dark-mode" />
</a>
<div class="sponsor-tagline" style="display:inline;">
<%= organization.sponsorship_tagline %>
<a class="partner-link sponsor-learn-more" href="<%= organization.sponsorship_url %>" target="_blank" data-details="<%= organization.slug %>__learn-more">
Learn More
</a>
</div>
</div>

View file

@ -35,6 +35,16 @@
</div>
</div>
<% end %>
<% if @tag_model.sponsor_organization_id && @tag_model.sponsorship_status == "live" %>
<div class="widget">
<header>
<h4><%= @tag %> is sponsored by</h4>
</header>
<div class="widget-body">
<%= render "articles/single_sponsor", organization: @tag_model.sponsor_organization %>
</div>
</div>
<% end %>
<% if @moderators.present? %>
<div class="widget">
<header>

View file

@ -22,7 +22,14 @@
<% end %>
<% if @credits.size > 0 %>
<center>
<a href="/listings/new">Create a Listing</a>
<p>
<a href="/listings/new">Create a Listing</a>
</p>
<% if @organizations.present? %>
<p>
<a href="/partnerships">View Partnership Opportunities</a>
</p>
<% end %>
</center>
<% end %>
</div>

View file

@ -1,6 +1,11 @@
<div id="sidebar-wrapper-right" class="sidebar-wrapper sidebar-wrapper-right">
<div class="sidebar-bg" id="sidebar-bg-right"></div>
<div class="side-bar sidebar-additional showing" id="sidebar-additional">
<% if @organization.sponsorship_status == "live" %>
<div class="sidebar-sponsorship-level" style="background:<%= @organization.decorate.sponsorship_color_hex %>">
<%= @organization.sponsorship_level.capitalize %> Community Sponsor ❤️
</div>
<% end %>
<% if @organization.users.present? %>
<div class="widget">
<div class="widget-suggested-follows-container">

View file

@ -15,6 +15,10 @@
display: inline;
left: 0;
}
.sponsor-level-headline {
padding:5px 18px;
border-radius: 3px;
}
</style>
<div class="container article">
@ -28,13 +32,12 @@
<h4>
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="golden badge">
<span style="background-color: #fefa87; padding:3px;">Gold Sponsors</span>
<span class="sponsor-level-headline" style="background-color: #fefa87;">Gold Sponsors</span>
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="golden badge">
</h4>
<div id="gold-sponsors">
<% Organization.where(is_gold_sponsor: true).order("sponsorship_featured_number ASC").each do |organization| %>
<% Organization.where(sponsorship_level: "gold").order("sponsorship_featured_number ASC").each do |organization| %>
<a href="<%= organization.sponsorship_url %>"><img src="<%= cloudinary(organization.nav_image_url, 250) %>"></a>
<p>
<%= organization.sponsorship_blurb_html.html_safe %>
</p>
@ -42,6 +45,37 @@
<hr />
<% end %>
</div>
<h4>
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="silver badge">
<span class="sponsor-level-headline" style="background-color: #d8e1f0;">Silver Sponsors</span>
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="silver badge">
</h4>
<div id="silver-sponsors">
<% Organization.where(sponsorship_level: "silver").order("sponsorship_featured_number ASC").each do |organization| %>
<a href="<%= organization.sponsorship_url %>"><img src="<%= cloudinary(organization.nav_image_url, 250) %>"></a>
<p>
<%= organization.sponsorship_blurb_html.html_safe %>
</p>
<hr />
<% end %>
</div>
<h4>
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="bronze badge">
<span class="sponsor-level-headline" style="background-color: #fcd6bb;">Bronze Sponsors</span>
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="bronze badge">
</h4>
<div id="bronze-sponsors">
<% Organization.where(sponsorship_level: "bronze").order("sponsorship_featured_number ASC").each do |organization| %>
<a href="<%= organization.sponsorship_url %>"><img src="<%= cloudinary(organization.nav_image_url, 250) %>"></a>
<p>
<%= organization.sponsorship_blurb_html.html_safe %>
</p>
<hr />
<% end %>
</div>
<h5> Interested in sponsoring dev.to? Email <a href="mailto:peter@dev.to">peter@dev.to</a></h5>
</div>
</div>

View file

@ -0,0 +1,5 @@
<h1>Bronze Sponsorship</h1>
<h3>🚀 Info</h3>
<p>
TBD
</p>

View file

@ -0,0 +1,5 @@
<h1>Editorial Consultation</h1>
<h3>🚀 Info</h3>
<p>
TBD
</p>

View file

@ -0,0 +1,122 @@
<% org = current_user.admin_organizations.first %>
<% if org == nil %>
<div class="partner-credits-explainer">
<h4>What next?</h4>
<h3>Create an Organization</h3>
<p>
<em>Organizations act as a publication and a way for your team to interact with DEV on behalf of the company</em>
</p>
<p>
Come back to this page once you have created an organization. Only organization <em>admins</em> may purchase sponsorships.
</p>
<a href="/settings/organization">Create Organization</a>
</div>
<% else %>
<% current_user.admin_organizations.each do |org| %>
<% if org.credits.where(spent: false).size < sponsorship_credits_price(level) %>
<div class="partner-credits-explainer">
<h4>What next?</h4>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" /> Purchase Credits for @<%= org.slug %></h3>
<% if org.credits.where(spent: false).size > 0 %>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<% end %>
<p>
<em>Credits are your wallet for flexibly managing all your DEV partnerships.</em>
</p>
<p>
Credits can be purchased via company credit card, and once in your account you can use them for different promotions.
</p>
<p>
Credits can be purchased in bulk for discounts on all future sponsorships and partnerships, or in small batches to test the waters.
</p>
<a href="/credits/purchase?organization_id=<%= org.id %>" data-no-instant>Get Credits</a>
</div>
<% else %>
<div class="partner-credits-explainer">
<% if %w[gold silver bronze].include?(level) %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<% if org.sponsorship_level == level %>
<div class="partner-explainer-notice">
🎉 You are Subscribed as a <%= level.capitalize %> Sponsor.
<% if org.sponsorship_status == "pending" %>
<br /><br />Your placement is <em>pending</em>.
<% end %>
</div>
<% elsif org.sponsorship_level.present? %>
<div class="partner-explainer-notice">
🛑 You are already subscribed as a <%= org.sponsorship_level %> sponsor. Contact partnerships@dev.to to change plans.
</div>
<% else %>
<h2><%= level.capitalize %> Sponsor Subscription</h2>
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>
<%= hidden_field_tag(:sponsorship_level, level) %>
<h4>Sponsorship message/brand instructions:</h4>
<%= text_area_tag(:sponsorship_instructions, nil, placeholder: "Include brand guideline links and detailed instructions about the message you are trying to get across. DEV editors will use these guidelines contextually depending on sponsorship.") %>
<button>Subscribe for <%= sponsorship_credits_price(level) %> credits</button>
<p style="font-size: 0.88em;">
<em>
This subscription will renew every month with credits in your account. If there are no credits, 50 credits will be purchased at the appropriate price. You can cancel this automatic sponsorship subscription at any time.
</em>
</p>
<% end %>
<% end %>
<% elsif level == "tag" %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<br />
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>
<%= hidden_field_tag(:sponsorship_level, level) %>
<% Tag.where(sponsor_organization_id: org.id).each do |tag| %>
<div class="partner-explainer-notice">
🎉 You are Subscribed as the sponsor of #<%= tag.name %>.
</div>
<% end %>
<%= select_tag "tag_name", options_for_select(Tag.where(sponsor_organization_id: nil, supported: true).pluck(:name)) %>
<button>Sponsor Tag</button>
<p style="font-size: 0.88em;">
<em>
This subscription will renew every month with credits in your account. If there are no credits, 50 credits will be purchased at the appropriate price. You can cancel this automatic sponsorship subscription at any time.
</em>
</p>
<% end %>
<% elsif level == "media" %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<br />
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>
<%= hidden_field_tag(:sponsorship_level, level) %>
<%= number_field_tag(:sponsorship_amount, nil, placeholder: "Number of credits") %>
<button>Add spend to your media sponsorship</button>
<p style="font-size: 0.88em;">
<em>
Credits will be allotted flexibly use in DEV-produced podcasts, videos and other media content. You will be notified as it is being distributed.
</em>
</p>
<% end %>
<% elsif level == "editorial" %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<br />
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>
<%= hidden_field_tag(:sponsorship_level, level) %>
<button>Sign Up for Editorial Consulting</button>
<p style="font-size: 1.1em;">
You will be contacted by the DEV team within one day of signing up for this service.
</p>
<p style="font-size: 0.88em;">
<em>
This subscription will renew every month with credits in your account. If there are no credits, 50 credits will be purchased at the appropriate price. You can cancel this automatic sponsorship subscription at any time.
</em>
</p>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
<% end %>

View file

@ -0,0 +1,5 @@
<h1>Gold Sponsorship</h1>
<h3>🚀 Info</h3>
<p>
TBD
</p>

View file

@ -0,0 +1,5 @@
<h1>Media Sponsorship</h1>
<h3>🚀 Info</h3>
<p>
TBD
</p>

View file

@ -0,0 +1,5 @@
<h1>Silver Sponsorship</h1>
<h3>🚀 Info</h3>
<p>
TBD
</p>

View file

@ -0,0 +1,5 @@
<h1>Tag Sponsorship</h1>
<h3>🚀 Info</h3>
<p>
TBD
</p>

View file

@ -0,0 +1,122 @@
<% title "Sponsors" %>
<style>
<% cache "partnership-org-section-credits-#{ApplicationConfig['DEPLOYMENT_SIGNATURE']}", expires_in: 10.hours do %>
<%= Rails.application.assets["partnerships.css"].to_s.html_safe %>
<% end %>
</style>
<div class="partnerships-container">
<h1>Partner With <img src="<%= asset_path "rainbowdev.svg" %>" /> </h1>
<h3>🚀 Reach</h3>
<p>
DEV reaches over 3.5 million unique visitors per month with highly technical content. That includes signed up members (<%= number_with_delimiter User.count %>) and yet-to-sign-up visitors from the open web.
</p>
<h3>❤️ Community</h3>
<p>
DEV is truly a leading community for software development and is leading the way to a more inclusive industry where everyone is welcome.
</p>
<h3>💎 Quality</h3>
<p>
DEV does not overwhelm members with popup messages, irrelevent ads or anything else that sours the user experience. We treat each visitor with care.
<h3>🤓 Expertise</h3>
<p>
DEV has experts on staff to help you craft your message to the community centered around and create positive mutual outcomes.
</p>
<div class="partnership-options">
<div class="partnership-option-card">
<h2>Bronze Sponsorship</h2>
<ul>
<li>
👀 Recognition on sponsors page (logo only)
</li>
<li>
🙌 Placement in monthly sponsor-awareness thread (name only)
</li>
<li>
🎉 Placement in home-page "sponsor awareness box" (rotation)
</li>
</ul>
<a class="partnership-option-cta" href="/partnerships/bronze-sponsor">Full Details</a>
</div>
<div class="partnership-option-card">
<h2>Silver Sponsorship</h2>
<ul>
<li>
👀 Recognition on sponsors page (logo + short blurb)
</li>
<li>
🙌 Placement in monthly sponsor-awareness thread (logo + short blurb)
</li>
<li>
🎉 Placement in home-page "sponsor awareness box" (rotation)
</li>
</ul>
<a class="partnership-option-cta" href="/partnerships/silver-sponsor">Full Details</a>
</div>
<div class="partnership-option-card">
<h2>Gold Sponsorship</h2>
<ul>
<li>
👀 Recognition on sponsors page (logo + full writeup)
</li>
<li>
🙌 Full-time home page brand placement + tagline + link
</li>
<li>
🎉 Shoutout + article placement in weekly newsletter
</li>
</ul>
<a class="partnership-option-cta" href="/partnerships/gold-sponsor">Full Details</a>
</div>
<div class="partnership-option-card">
<h2>Tag Sponsorship</h2>
<ul>
<li>
👀 Full Writeup on tag sidebar
</li>
<li>
🙌 Recognition in tags overview page
</li>
<li>
🎉 Recognition on organization profile
</li>
</ul>
<a class="partnership-option-cta" href="/partnerships/tag-sponsor">Full Details</a>
</div>
<div class="partnership-option-card">
<h2>Media Sponsorship</h2>
<ul>
<li>
👀 Sponsor custom productions such as video and podcasts
</li>
<li>
🙌 Pre-purchase for agile placement in new things
</li>
<li>
🎉 Creative copy written by DEV team
</li>
</ul>
<a class="partnership-option-cta" href="/partnerships/media-sponsor">Full Details</a>
</div>
<div class="partnership-option-card">
<h2>Editorial Consultation</h2>
<ul>
<li>
👀 Weekly editorial notes and suggestions to help your devrels make the most of DEV
</li>
<li>
🙌 First notice of new on-site opportunities and events
</li>
<li>
🎉 Premium support
</li>
</ul>
<a class="partnership-option-cta" href="/partnerships/editorial-consult">Full Details</a>
</div>
</div>
<div style="text-align: center;">
<h2>Questions about anything?<br /><br />Email <a href="mailto:partners@dev.to">partners@dev.to</a></h1>
</div>
</div>

View file

@ -0,0 +1,31 @@
<% title "#{params[:option].titleize} Overview" %>
<style>
<% cache "partnership-org-section-credits-#{ApplicationConfig['DEPLOYMENT_SIGNATURE']}", expires_in: 10.hours do %>
<%= Rails.application.assets["partnerships.css"].to_s.html_safe %>
<% end %>
</style>
<div class="partnerships-container partnerships-container-show">
<p><a href="/partnerships">👈 back to partnerships list</a></p>
<% if params[:option] == "gold-sponsor" %>
<%= render "gold_sponsor_copy" %>
<% elsif params[:option] == "silver-sponsor" %>
<%= render "silver_sponsor_copy" %>
<% elsif params[:option] == "bronze-sponsor" %>
<%= render "bronze_sponsor_copy" %>
<% elsif params[:option] == "tag-sponsor" %>
<%= render "tag_sponsor_copy" %>
<% elsif params[:option] == "media-sponsor" %>
<%= render "media_sponsor_copy" %>
<% elsif params[:option] == "editorial-consult" %>
<%= render "editorial_consult_copy" %>
<% end %>
<% if user_signed_in? %>
<%= render "form", level: params[:option].split("-")[0] %>
<% else %>
<div class="partner-credits-explainer">
<a href="/enter">Sign in to get started</a>
</div>
<% end %>
</div>

View file

@ -150,6 +150,7 @@ Rails.application.routes.draw do
resources :poll_votes, only: %i[show create]
resources :poll_skips, only: [:create]
resources :profile_pins, only: %i[create update]
resources :partnerships, only: %i[index create]
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"
get "/credits/purchase" => "credits#new"
@ -171,6 +172,8 @@ Rails.application.routes.draw do
post "/chat_channels/create_chat" => "chat_channels#create_chat"
post "/chat_channels/block_chat" => "chat_channels#block_chat"
get "/live/:username" => "twitch_live_streams#show"
get "/partnerships/:option" => "partnerships#show"
post "/pusher/auth" => "pusher#auth"

View file

@ -0,0 +1,11 @@
class AddAdditionalSponsorOptionsToOrganizations < ActiveRecord::Migration[5.2]
def change
add_column :organizations, :sponsorship_level, :string
add_column :organizations, :sponsorship_expires_at, :datetime
add_column :organizations, :sponsorship_status, :string, default: "none"
add_column :organizations, :sponsorship_instructions, :text, default: ""
add_column :organizations, :sponsorship_instructions_updated_at, :datetime
add_column :tags, :sponsor_organization_id, :integer
add_column :tags, :sponsorship_status, :string, default: "none"
end
end

View file

@ -586,7 +586,12 @@ ActiveRecord::Schema.define(version: 2019_07_03_003817) do
t.string "slug"
t.integer "spent_credits_count", default: 0, null: false
t.text "sponsorship_blurb_html"
t.datetime "sponsorship_expires_at"
t.integer "sponsorship_featured_number", default: 0
t.text "sponsorship_instructions", default: ""
t.datetime "sponsorship_instructions_updated_at"
t.string "sponsorship_level"
t.string "sponsorship_status", default: "none"
t.string "sponsorship_tagline"
t.string "sponsorship_url"
t.string "state"
@ -858,6 +863,8 @@ ActiveRecord::Schema.define(version: 2019_07_03_003817) do
t.text "rules_markdown"
t.string "short_summary"
t.string "social_image"
t.integer "sponsor_organization_id"
t.string "sponsorship_status", default: "none"
t.string "submission_rules_headsup"
t.text "submission_template"
t.boolean "supported", default: false

View file

@ -0,0 +1,158 @@
require "rails_helper"
RSpec.describe "Pages", type: :request do
describe "GET /partnerships" do
context "when user is logged in" do
before do
get "/partnerships"
end
it "renders main text" do
expect(response.body).to include "Partner With"
end
end
context "when user is not logged in" do
before do
user = create(:user)
sign_in user
get "/partnerships"
end
it "renders main text" do
expect(response.body).to include "Partner With"
end
end
end
describe "GET /partnerships/:show" do
let(:user) { create(:user) }
context "when user is logged in" do
before do
sign_in user
end
describe "shared basic functionality" do
it "gets bronze sponsorship page" do
get "/partnerships/bronze-sponsor"
expect(response.body).to include("Bronze Sponsorship")
end
it "asks user to create org if not created" do
get "/partnerships/bronze-sponsor"
expect(response.body).to include("Create an Organization")
end
it "asks user to purchase credits if not purchased" do
organization = create(:organization)
OrganizationMembership.create(user_id: user.id, organization_id: organization.id, type_of_user: "admin")
get "/partnerships/bronze-sponsor"
expect(response.body).to include("Purchase Credits")
end
it "includes sponsorship form if organization has credits" do
organization = create(:organization)
OrganizationMembership.create(user_id: user.id, organization_id: organization.id, type_of_user: "admin")
Credit.add_to_org(organization, 100)
get "/partnerships/bronze-sponsor"
expect(response.body).to include("This subscription will renew every month")
end
end
end
context "when user is not logged in" do
describe "shared basic functionality" do
it "gets bronze sponsorship page" do
get "/partnerships/bronze-sponsor"
expect(response.body).to include("Bronze Sponsorship")
expect(response.body).to include("Sign in to get started")
end
end
end
end
describe "POST /partnerships" do
let(:user) { create(:user) }
let(:organization) { create(:organization) }
context "when user is logged in as an admin and has enough credits" do
before do
OrganizationMembership.create(user_id: user.id, organization_id: organization.id, type_of_user: "admin")
Credit.add_to_org(organization, 2000)
sign_in user
end
it "subscribes to bronze sponsorship" do
post "/partnerships", params: {
sponsorship_level: "bronze",
organization_id: organization.id
}
expect(organization.reload.sponsorship_level).to eq("bronze")
end
it "subscribes to silver sponsorship" do
post "/partnerships", params: {
sponsorship_level: "silver",
organization_id: organization.id
}
expect(organization.reload.sponsorship_level).to eq("silver")
end
xit "subscribes to gold sponsorship" do # skipped for now do to high credit need (not sure making this self serve makes sense)
post "/partnerships", params: {
sponsorship_level: "gold",
organization_id: organization.id
}
expect(organization.reload.sponsorship_level).to eq("gold")
end
it "subscribes to editorial subscription" do
post "/partnerships", params: {
sponsorship_level: "editorial",
organization_id: organization.id
}
expect(organization.reload.credits.where(spent: false).size).to eq(1500)
end
it "subscribes to media sponsorship" do
post "/partnerships", params: {
sponsorship_level: "media",
organization_id: organization.id,
sponsorship_amount: 900
}
expect(organization.reload.credits.where(spent: false).size).to eq(1100)
end
it "subscribes to tag sponsorship" do
tag = create(:tag)
post "/partnerships", params: {
sponsorship_level: "tag",
organization_id: organization.id,
sponsorship_amount: 900,
tag_name: tag.name
}
expect(organization.reload.credits.where(spent: false).size).to eq(1500)
expect(tag.reload.sponsor_organization_id).to eq(organization.id)
end
it "updates sponsorship instructions if new instructions" do
post "/partnerships", params: {
sponsorship_level: "bronze",
organization_id: organization.id,
sponsorship_instructions: "hello there"
}
expect(organization.reload.sponsorship_instructions).to include("hello there")
expect(organization.reload.sponsorship_instructions_updated_at).to be > 30.seconds.ago
end
end
context "when user is logged in as a non-admin" do
before do
OrganizationMembership.create(user_id: user.id, organization_id: organization.id, type_of_user: "member")
Credit.add_to_org(organization, 2000)
sign_in user
end
it "subscribes to bronze sponsorship" do
expect do
post "/partnerships", params: {
sponsorship_level: "bronze",
organization_id: organization.id
}
end.to raise_error(Pundit::NotAuthorizedError)
end
end
end
end

View file

@ -42,13 +42,13 @@ RSpec.describe "StoriesIndex", type: :request do
end
it "has sponsors displayed" do
org = create(:organization, is_gold_sponsor: true, sponsorship_tagline: "Oh Yeah!!!")
org = create(:organization, sponsorship_level: "gold", sponsorship_tagline: "Oh Yeah!!!", sponsorship_status: "live")
get "/"
expect(response.body).to include(org.sponsorship_tagline)
end
it "does not display non-sponsor org" do
org = create(:organization, is_gold_sponsor: false, sponsorship_tagline: "Oh Yeah!!!")
org = create(:organization, sponsorship_level: nil, sponsorship_tagline: "Oh Yeah!!!")
get "/"
expect(response.body).not_to include(org.sponsorship_tagline)
end
@ -101,5 +101,21 @@ RSpec.describe "StoriesIndex", type: :request do
get "/t/#{tag2.name}"
expect(response.body).to redirect_to "/t/#{tag.name}"
end
it "does not render sponsor if not live" do
organization = create(:organization, sponsorship_tagline: "Oh Yeah!!!")
tag = create(:tag, sponsor_organization_id: organization.id)
get "/t/#{tag.name}"
expect(response.body).not_to include("is sponsored by")
expect(response.body).not_to include(organization.sponsorship_tagline)
end
it "renders sponsor" do
organization = create(:organization, sponsorship_tagline: "Oh Yeah!!!!!!")
tag = create(:tag, sponsor_organization_id: organization.id, sponsorship_status: "live")
get "/t/#{tag.name}"
expect(response.body).to include("is sponsored by")
expect(response.body).to include(organization.sponsorship_tagline)
end
end
end

View file

@ -38,16 +38,28 @@ RSpec.describe "UserProfiles", type: :request do
get "/#{old_username}"
expect(response).to redirect_to("/#{user.username}")
end
context "organizationd" do
it "renders organization page if org" do
get organization.path
expect(response.body).to include CGI.escapeHTML(organization.name)
end
it "renders organization page if org" do
get organization.path
expect(response.body).to include CGI.escapeHTML(organization.name)
end
it "renders organization users on sidebar" do
create(:organization_membership, user_id: user.id, organization_id: organization.id)
get organization.path
expect(response.body).to include user.profile_image_url
end
it "renders organization users on sidebar" do
create(:organization_membership, user_id: user.id, organization_id: organization.id)
get organization.path
expect(response.body).to include user.profile_image_url
it "renders no sponsors if not sponsor" do
get organization.path
expect(response.body).not_to include "Gold Community Sponsor"
end
it "renders sponsor if it is sponsored" do
organization.update_columns(sponsorship_level: "gold", sponsorship_status: "live")
get organization.reload.path
expect(response.body).to include "Gold Community Sponsor"
end
end
end