diff --git a/docs/plans/2026-01-21-card-boosts-design.md b/docs/plans/2026-01-21-card-boosts-design.md deleted file mode 100644 index c828a66b5..000000000 --- a/docs/plans/2026-01-21-card-boosts-design.md +++ /dev/null @@ -1,741 +0,0 @@ -# Card Boosts Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Extend the Reaction model to support reactions on Cards (not just Comments), with a compact boost count on card previews and full reaction UI in the card detail footer. - -**Architecture:** Make Reaction polymorphic with `reactable_type`/`reactable_id`. Create `Cards::ReactionsController` mirroring the existing comment reactions controller. Add views for card reactions in the footer, and a compact count display on card previews. - -**Tech Stack:** Rails 8, Turbo, Stimulus, ERB views - ---- - -## Task 1: Migration - Make Reaction Polymorphic - -**Files:** -- Create: `db/migrate/YYYYMMDDHHMMSS_make_reactions_polymorphic.rb` - -**Step 1: Generate and write the migration** - -Run: -```bash -bin/rails generate migration MakeReactionsPolymorphic -``` - -Then edit the migration file: - -```ruby -class MakeReactionsPolymorphic < ActiveRecord::Migration[8.0] - def change - add_column :reactions, :reactable_type, :string - add_column :reactions, :reactable_id, :uuid - - reversible do |dir| - dir.up do - execute <<~SQL - UPDATE reactions SET reactable_type = 'Comment', reactable_id = comment_id - SQL - end - end - - change_column_null :reactions, :reactable_type, false - change_column_null :reactions, :reactable_id, false - - remove_column :reactions, :comment_id, :uuid - - add_index :reactions, [:reactable_type, :reactable_id] - end -end -``` - -**Step 2: Run the migration** - -Run: `bin/rails db:migrate` - -**Step 3: Verify migration succeeded** - -Run: `bin/rails runner "puts Reaction.column_names.inspect"` - -Expected: Should include `reactable_type` and `reactable_id`, should NOT include `comment_id` - -**Step 4: Commit** - -```bash -git add db/migrate/*_make_reactions_polymorphic.rb db/schema.rb -git commit -m "Make Reaction polymorphic for card and comment reactions" -``` - ---- - -## Task 2: Update Reaction Model - -**Files:** -- Modify: `app/models/reaction.rb` - -**Step 1: Write the failing test** - -Add to `test/models/reaction_test.rb`: - -```ruby -test "reaction can belong to a card" do - card = cards(:logo) - reaction = card.reactions.create!(content: "🎉") - - assert_equal card, reaction.reactable - assert_equal "Card", reaction.reactable_type -end - -test "creating card reaction touches card last_active_at" do - card = cards(:logo) - original_last_active_at = card.last_active_at - - travel 1.minute do - card.reactions.create!(content: "🚀") - end - - assert_operator card.reload.last_active_at, :>, original_last_active_at -end -``` - -**Step 2: Run the test to verify it fails** - -Run: `bin/rails test test/models/reaction_test.rb` - -Expected: FAIL - Card doesn't have `reactions` association yet - -**Step 3: Update the Reaction model** - -Replace `app/models/reaction.rb`: - -```ruby -class Reaction < ApplicationRecord - belongs_to :account, default: -> { reactable.account } - belongs_to :reactable, polymorphic: true, touch: true - belongs_to :reacter, class_name: "User", default: -> { Current.user } - - scope :ordered, -> { order(:created_at) } - - after_create :register_card_activity - - delegate :all_emoji?, to: :content - - private - def register_card_activity - reactable.card.touch_last_active_at - end -end -``` - -**Step 4: Run tests to verify they still fail (need Card association)** - -Run: `bin/rails test test/models/reaction_test.rb` - -Expected: FAIL - Card doesn't have `reactions` association - -**Step 5: Commit model change** - -```bash -git add app/models/reaction.rb test/models/reaction_test.rb -git commit -m "Update Reaction model to be polymorphic" -``` - ---- - -## Task 3: Update Comment Model - -**Files:** -- Modify: `app/models/comment.rb` - -**Step 1: Update Comment association** - -In `app/models/comment.rb`, change line 7 from: - -```ruby -has_many :reactions, -> { order(:created_at) }, dependent: :delete_all -``` - -to: - -```ruby -has_many :reactions, -> { order(:created_at) }, as: :reactable, dependent: :delete_all -``` - -**Step 2: Run existing comment reaction tests** - -Run: `bin/rails test test/controllers/cards/comments/reactions_controller_test.rb` - -Expected: All tests PASS - -**Step 3: Commit** - -```bash -git add app/models/comment.rb -git commit -m "Update Comment reactions association for polymorphic" -``` - ---- - -## Task 4: Add Reactions to Card Model - -**Files:** -- Modify: `app/models/card.rb` - -**Step 1: Add reactions association to Card** - -In `app/models/card.rb`, after the `has_many :comments` line (around line 10), add: - -```ruby -has_many :reactions, -> { order(:created_at) }, as: :reactable, dependent: :delete_all -``` - -**Step 2: Run the model tests** - -Run: `bin/rails test test/models/reaction_test.rb` - -Expected: All tests PASS - -**Step 3: Commit** - -```bash -git add app/models/card.rb -git commit -m "Add reactions association to Card model" -``` - ---- - -## Task 5: Add Routes for Card Reactions - -**Files:** -- Modify: `config/routes.rb` - -**Step 1: Write failing test for routes** - -Add to `test/controllers/cards/reactions_controller_test.rb` (new file): - -```ruby -require "test_helper" - -class Cards::ReactionsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :david - @card = cards(:logo) - end - - test "routes exist" do - assert_routing({ path: "/5986089/cards/#{@card.number}/reactions", method: :get }, - { controller: "cards/reactions", action: "index", account_id: "5986089", card_id: @card.number.to_s }) - assert_routing({ path: "/5986089/cards/#{@card.number}/reactions", method: :post }, - { controller: "cards/reactions", action: "create", account_id: "5986089", card_id: @card.number.to_s }) - end -end -``` - -**Step 2: Run test to verify it fails** - -Run: `bin/rails test test/controllers/cards/reactions_controller_test.rb` - -Expected: FAIL - No route matches - -**Step 3: Add routes** - -In `config/routes.rb`, inside the `resources :cards` block (around line 93), add after `resources :taggings`: - -```ruby -resources :reactions, module: :cards -``` - -So it looks like: - -```ruby -resources :taggings - -resources :reactions, module: :cards - -resources :comments do -``` - -**Step 4: Run test to verify routes exist** - -Run: `bin/rails test test/controllers/cards/reactions_controller_test.rb` - -Expected: FAIL - Controller doesn't exist (but routes work) - -**Step 5: Commit** - -```bash -git add config/routes.rb test/controllers/cards/reactions_controller_test.rb -git commit -m "Add routes for card reactions" -``` - ---- - -## Task 6: Create Cards::ReactionsController - -**Files:** -- Create: `app/controllers/cards/reactions_controller.rb` - -**Step 1: Add controller tests** - -Replace `test/controllers/cards/reactions_controller_test.rb`: - -```ruby -require "test_helper" - -class Cards::ReactionsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :david - @card = cards(:logo) - end - - test "index" do - get card_reactions_path(@card) - assert_response :success - end - - test "create" do - assert_difference -> { @card.reactions.count }, 1 do - post card_reactions_path(@card, format: :turbo_stream), params: { reaction: { content: "🎉" } } - assert_turbo_stream action: :replace, target: dom_id(@card, :reacting) - end - end - - test "destroy" do - reaction = @card.reactions.create!(content: "👍") - - assert_difference -> { @card.reactions.count }, -1 do - delete card_reaction_path(@card, reaction, format: :turbo_stream) - assert_turbo_stream action: :remove, target: dom_id(reaction) - end - end - - test "non-owner cannot destroy reaction" do - sign_in_as :kevin - reaction = @card.reactions.create!(content: "👍", reacter: users(:david)) - - assert_no_difference -> { @card.reactions.count } do - delete card_reaction_path(@card, reaction, format: :turbo_stream) - assert_response :forbidden - end - end - - test "create as JSON" do - assert_difference -> { @card.reactions.count }, 1 do - post card_reactions_path(@card), params: { reaction: { content: "👍" } }, as: :json - end - - assert_response :created - end - - test "destroy as JSON" do - reaction = @card.reactions.create!(content: "👍") - - assert_difference -> { @card.reactions.count }, -1 do - delete card_reaction_path(@card, reaction), as: :json - end - - assert_response :no_content - end -end -``` - -**Step 2: Run tests to verify they fail** - -Run: `bin/rails test test/controllers/cards/reactions_controller_test.rb` - -Expected: FAIL - Controller doesn't exist - -**Step 3: Create the controller** - -Create `app/controllers/cards/reactions_controller.rb`: - -```ruby -class Cards::ReactionsController < ApplicationController - include CardScoped - - before_action :set_reaction, only: %i[ destroy ] - before_action :ensure_permision_to_administer_reaction, only: %i[ destroy ] - - def index - end - - def new - end - - def create - @reaction = @card.reactions.create!(params.expect(reaction: :content)) - - respond_to do |format| - format.turbo_stream - format.json { head :created } - end - end - - def destroy - @reaction.destroy - - respond_to do |format| - format.turbo_stream - format.json { head :no_content } - end - end - - private - def set_reaction - @reaction = @card.reactions.find(params[:id]) - end - - def ensure_permision_to_administer_reaction - head :forbidden if Current.user != @reaction.reacter - end -end -``` - -**Step 4: Run tests (will fail due to missing views)** - -Run: `bin/rails test test/controllers/cards/reactions_controller_test.rb` - -Expected: FAIL - Missing template - -**Step 5: Commit controller** - -```bash -git add app/controllers/cards/reactions_controller.rb test/controllers/cards/reactions_controller_test.rb -git commit -m "Add Cards::ReactionsController" -``` - ---- - -## Task 7: Create Card Reactions Views - -**Files:** -- Create: `app/views/cards/reactions/_reactions.html.erb` -- Create: `app/views/cards/reactions/_reaction.html.erb` -- Create: `app/views/cards/reactions/_menu.html.erb` -- Create: `app/views/cards/reactions/index.html.erb` -- Create: `app/views/cards/reactions/new.html.erb` -- Create: `app/views/cards/reactions/create.turbo_stream.erb` -- Create: `app/views/cards/reactions/destroy.turbo_stream.erb` - -**Step 1: Create the views directory** - -Run: `mkdir -p app/views/cards/reactions` - -**Step 2: Create `_reactions.html.erb`** - -```erb -<%= turbo_frame_tag card, :reacting do %> -