diff --git a/app/assets/stylesheets/pages/certification/ships/_index.scss b/app/assets/stylesheets/pages/certification/ships/_index.scss index 4435b3558..06502d633 100644 --- a/app/assets/stylesheets/pages/certification/ships/_index.scss +++ b/app/assets/stylesheets/pages/certification/ships/_index.scss @@ -710,6 +710,19 @@ } } + &__bonus-tag { + display: inline-flex; + align-items: center; + padding: 1px 7px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.02em; + white-space: nowrap; + background: rgba(255, 229, 100, 0.15); + color: var(--color-brand-yellow); + } + // --- Empty state ------------------------------------------------------- &__empty { padding: var(--space-xl); diff --git a/app/assets/stylesheets/pages/certification/ships/_show.scss b/app/assets/stylesheets/pages/certification/ships/_show.scss index 8b766b08e..43180db53 100644 --- a/app/assets/stylesheets/pages/certification/ships/_show.scss +++ b/app/assets/stylesheets/pages/certification/ships/_show.scss @@ -395,6 +395,76 @@ } } +.bonus-stardust { + border-color: rgba(255, 229, 100, 0.25) !important; + background: rgba(255, 229, 100, 0.04) !important; + + &__current { + margin: 0 0 var(--space-s); + font-size: 0.95rem; + color: var(--color-brand-yellow); + font-weight: 600; + + strong { + font-weight: 800; + } + } + + &__form { + margin-bottom: var(--space-xs); + } + + &__field { + display: flex; + gap: var(--space-xs); + align-items: center; + } + + &__input { + flex: 1; + padding: var(--space-xs) var(--space-s); + font-family: inherit; + font-size: var(--font-size-m); + font-variant-numeric: tabular-nums; + color: var(--color-space-text); + background: var(--color-space-surface, rgba(255, 255, 255, 0.04)); + border: 1px solid rgba(255, 229, 100, 0.3); + border-radius: 8px; + appearance: textfield; + + &:focus-visible { + outline: 2px solid var(--color-brand-yellow); + outline-offset: 1px; + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + + &__breakdown { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: var(--space-s); + padding-top: var(--space-s); + border-top: 1px solid rgba(255, 229, 100, 0.15); + font-size: 0.85rem; + } + + &__breakdown-label { + color: var(--color-space-text-muted, rgba(255, 255, 255, 0.5)); + } + + &__breakdown-value { + font-weight: 700; + color: var(--color-brand-yellow); + font-variant-numeric: tabular-nums; + } +} + @keyframes ship-review-pop { 0% { transform: scale(1); diff --git a/app/controllers/admin/certification/ships_controller.rb b/app/controllers/admin/certification/ships_controller.rb index de3bc6ad9..67f8112bf 100644 --- a/app/controllers/admin/certification/ships_controller.rb +++ b/app/controllers/admin/certification/ships_controller.rb @@ -1,6 +1,6 @@ class Admin::Certification::ShipsController < Admin::Certification::ApplicationController before_action :release_other_claims, only: [ :next ] - before_action :set_ship, only: [ :show, :update, :set_project_type, :report_fraud ] + before_action :set_ship, only: [ :show, :update, :set_project_type, :set_bonus_stardust, :report_fraud ] before_action :set_submitter_context, only: [ :show, :update ] before_action :set_body_class, only: [ :index, :show, :update, :logs ] @@ -117,6 +117,12 @@ def set_project_type end end + def set_bonus_stardust + authorize @ship + @ship.update!(bonus_stardust: params[:bonus_stardust].presence) + redirect_to admin_certification_ship_path(@ship), notice: "Bonus stardust updated." + end + def update authorize @ship if internal_sw_dash_reviews_disabled? && @ship.external_certification_id.present? diff --git a/app/models/certification/ship.rb b/app/models/certification/ship.rb index c43c3d983..29836982a 100644 --- a/app/models/certification/ship.rb +++ b/app/models/certification/ship.rb @@ -3,6 +3,7 @@ # Table name: certification_ship_reviews # # id :bigint not null, primary key +# bonus_stardust :float # claim_expires_at :datetime # claimed_at :datetime # decided_at :datetime @@ -144,6 +145,9 @@ def transfer_external_certification_id_to!(other) allow_blank: true validates :verdict_video, content_type: { in: ACCEPTED_VIDEO_TYPES, spoofing_protection: true } + validates :bonus_stardust, + numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, + allow_nil: true scope :for_reviewer, ->(user) { joins(:project) @@ -393,12 +397,13 @@ def decided_on decided_at || updated_at end + private def assign_stardust_earned total_count = Certification::Ship.decided_today_count(reviewer_id) + 1 multiplier = Certification::Ship.multiplier_for_milestone(total_count) - self.stardust_earned = REVIEW_BOUNTY * multiplier + self.stardust_earned = (REVIEW_BOUNTY * multiplier) + (bonus_stardust || 0) end def stamp_claimed_at diff --git a/app/policies/admin/certification/ship_policy.rb b/app/policies/admin/certification/ship_policy.rb index fa8fb6c0f..99f2ebea6 100644 --- a/app/policies/admin/certification/ship_policy.rb +++ b/app/policies/admin/certification/ship_policy.rb @@ -13,6 +13,8 @@ def next? = user&.can_review? def set_project_type? = show? + def set_bonus_stardust? = user&.admin? + def report_fraud? = user&.can_review? class Scope < ApplicationPolicy::Scope diff --git a/app/views/admin/certification/ships/index.html.erb b/app/views/admin/certification/ships/index.html.erb index 0c4d5972c..84d680a1c 100644 --- a/app/views/admin/certification/ships/index.html.erb +++ b/app/views/admin/certification/ships/index.html.erb @@ -237,6 +237,9 @@