diff --git a/app/assets/stylesheets/articles.scss b/app/assets/stylesheets/articles.scss
index c05c2384d..b267185a4 100644
--- a/app/assets/stylesheets/articles.scss
+++ b/app/assets/stylesheets/articles.scss
@@ -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;
diff --git a/app/assets/stylesheets/partnerships.scss b/app/assets/stylesheets/partnerships.scss
new file mode 100644
index 000000000..39d1e905e
--- /dev/null
+++ b/app/assets/stylesheets/partnerships.scss
@@ -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
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/controllers/partnerships_controller.rb b/app/controllers/partnerships_controller.rb
new file mode 100644
index 000000000..e1ba95987
--- /dev/null
+++ b/app/controllers/partnerships_controller.rb
@@ -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
diff --git a/app/dashboards/organization_dashboard.rb b/app/dashboards/organization_dashboard.rb
index bd5616498..592abe0e0 100644
--- a/app/dashboards/organization_dashboard.rb
+++ b/app/dashboards/organization_dashboard.rb
@@ -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
diff --git a/app/dashboards/tag_dashboard.rb b/app/dashboards/tag_dashboard.rb
index c51054247..49c874d01 100644
--- a/app/dashboards/tag_dashboard.rb
+++ b/app/dashboards/tag_dashboard.rb
@@ -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
diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb
index 348f99730..bf7053b3e 100644
--- a/app/decorators/organization_decorator.rb
+++ b/app/decorators/organization_decorator.rb
@@ -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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ace98fe5a..5fbcd7e5a 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/models/organization.rb b/app/models/organization.rb
index ad6a17842..84243a7a8 100644
--- a/app/models/organization.rb
+++ b/app/models/organization.rb
@@ -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 }
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 03606ab5c..2b419d1d3 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -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
diff --git a/app/policies/organization_policy.rb b/app/policies/organization_policy.rb
index 3034230b4..df2a6a37c 100644
--- a/app/policies/organization_policy.rb
+++ b/app/policies/organization_policy.rb
@@ -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
diff --git a/app/views/articles/_sidebar.html.erb b/app/views/articles/_sidebar.html.erb
index b0cfed846..26b164d0d 100644
--- a/app/views/articles/_sidebar.html.erb
+++ b/app/views/articles/_sidebar.html.erb
@@ -10,22 +10,11 @@
" alt="emoji heart" />
- <% @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? %>